From abaf51eb1f2ce8e9e20406e7fcde7c382af3c81f Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Tue, 7 Jun 2022 23:47:04 +0200 Subject: [PATCH 01/17] Tests: Add tests for lockfile version (#2106) * Add tests for lockfile version * add resolveJsonModule for typecheck --- src/__tests__/lockfile-version.test.ts | 10 ++++++++++ tsconfig.json | 1 + 2 files changed, 11 insertions(+) create mode 100644 src/__tests__/lockfile-version.test.ts diff --git a/src/__tests__/lockfile-version.test.ts b/src/__tests__/lockfile-version.test.ts new file mode 100644 index 00000000000..b41e55fd96b --- /dev/null +++ b/src/__tests__/lockfile-version.test.ts @@ -0,0 +1,10 @@ +import lockFile from '../../package-lock.json' +import docsLockFile from '../../docs/package-lock.json' + +test('root: lockfileVersion should be 2', async () => { + expect(lockFile.lockfileVersion).toEqual(2) +}) + +test('docs: lockfileVersion should be 2', async () => { + expect(docsLockFile.lockfileVersion).toEqual(2) +}) diff --git a/tsconfig.json b/tsconfig.json index 7bb26edce02..d38ad0424d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, "typeRoots": ["./node_modules/@types", "./@types"] }, "include": ["@types/**/*.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.tsx", "script/**/*.ts"] From 6e3532cf8ca11f6edc08e101d9cea4df6a1655ce Mon Sep 17 00:00:00 2001 From: Dusty Greif Date: Tue, 7 Jun 2022 17:11:03 -0700 Subject: [PATCH 02/17] Allow minor version updates for prod dependencies (#2117) * Allow minor version updates for prod dependencies * Create proud-colts-attend.md --- .changeset/proud-colts-attend.md | 5 +++++ package.json | 36 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 .changeset/proud-colts-attend.md diff --git a/.changeset/proud-colts-attend.md b/.changeset/proud-colts-attend.md new file mode 100644 index 00000000000..bd60cd15265 --- /dev/null +++ b/.changeset/proud-colts-attend.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +Allow minor version updates for production dependencies diff --git a/package.json b/package.json index 4a9684ec400..f77a2ffea2d 100644 --- a/package.json +++ b/package.json @@ -78,24 +78,24 @@ "npm": ">=7" }, "dependencies": { - "@primer/behaviors": "1.1.1", - "@primer/octicons-react": "16.1.1", - "@primer/primitives": "7.6.0", - "@radix-ui/react-polymorphic": "0.0.14", - "@react-aria/ssr": "3.1.0", - "@styled-system/css": "5.1.5", - "@styled-system/props": "5.1.5", - "@styled-system/theme-get": "5.1.2", - "@types/styled-components": "5.1.11", - "@types/styled-system": "5.1.12", - "@types/styled-system__css": "5.0.16", - "@types/styled-system__theme-get": "5.0.1", - "classnames": "2.3.1", - "color2k": "1.2.4", - "deepmerge": "4.2.2", - "focus-visible": "5.2.0", - "history": "5.0.0", - "styled-system": "5.1.5" + "@primer/behaviors": "^1.1.1", + "@primer/octicons-react": "^16.1.1", + "@primer/primitives": "^7.6.0", + "@radix-ui/react-polymorphic": "^0.0.14", + "@react-aria/ssr": "^3.1.0", + "@styled-system/css": "^5.1.5", + "@styled-system/props": "^5.1.5", + "@styled-system/theme-get": "^5.1.2", + "@types/styled-components": "^5.1.11", + "@types/styled-system": "^5.1.12", + "@types/styled-system__css": "^5.0.16", + "@types/styled-system__theme-get": "^5.0.1", + "classnames": "^2.3.1", + "color2k": "^1.2.4", + "deepmerge": "^4.2.2", + "focus-visible": "^5.2.0", + "history": "^5.0.0", + "styled-system": "^5.1.5" }, "devDependencies": { "@babel/cli": "7.17.6", From 68395e7d1575e62e82ab7feb508af781f99a99c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jun 2022 10:28:09 -0700 Subject: [PATCH 03/17] chore(deps): bump @primer/octicons-react from 16.1.1 to 17.3.0 (#2119) Bumps [@primer/octicons-react](https://github.com/primer/octicons) from 16.1.1 to 17.3.0. - [Release notes](https://github.com/primer/octicons/releases) - [Changelog](https://github.com/primer/octicons/blob/main/CHANGELOG.md) - [Commits](https://github.com/primer/octicons/compare/v16.1.1...v17.3.0) --- updated-dependencies: - dependency-name: "@primer/octicons-react" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 52 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index a747516ceb2..f1c1012e40d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,32 @@ { "name": "@primer/react", - "version": "35.2.1", + "version": "35.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@primer/react", - "version": "35.2.1", + "version": "35.2.2", "license": "MIT", "dependencies": { - "@primer/behaviors": "1.1.1", - "@primer/octicons-react": "16.1.1", - "@primer/primitives": "7.6.0", - "@radix-ui/react-polymorphic": "0.0.14", - "@react-aria/ssr": "3.1.0", - "@styled-system/css": "5.1.5", - "@styled-system/props": "5.1.5", - "@styled-system/theme-get": "5.1.2", - "@types/styled-components": "5.1.11", - "@types/styled-system": "5.1.12", - "@types/styled-system__css": "5.0.16", - "@types/styled-system__theme-get": "5.0.1", - "classnames": "2.3.1", - "color2k": "1.2.4", - "deepmerge": "4.2.2", - "focus-visible": "5.2.0", - "history": "5.0.0", - "styled-system": "5.1.5" + "@primer/behaviors": "^1.1.1", + "@primer/octicons-react": "^17.3.0", + "@primer/primitives": "^7.6.0", + "@radix-ui/react-polymorphic": "^0.0.14", + "@react-aria/ssr": "^3.1.0", + "@styled-system/css": "^5.1.5", + "@styled-system/props": "^5.1.5", + "@styled-system/theme-get": "^5.1.2", + "@types/styled-components": "^5.1.11", + "@types/styled-system": "^5.1.12", + "@types/styled-system__css": "^5.0.16", + "@types/styled-system__theme-get": "^5.0.1", + "classnames": "^2.3.1", + "color2k": "^1.2.4", + "deepmerge": "^4.2.2", + "focus-visible": "^5.2.0", + "history": "^5.0.0", + "styled-system": "^5.1.5" }, "devDependencies": { "@babel/cli": "7.17.6", @@ -5594,9 +5594,9 @@ "integrity": "sha512-wvF1PYjyxKNTr6+5w4uR5Gkz53t1fsRDgKjWxDKk7wmlh0cwiILBo4dDFjjVhWRF1mBSjaIxxJGB4WGaP7ct2Q==" }, "node_modules/@primer/octicons-react": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-16.1.1.tgz", - "integrity": "sha512-xCxQ5z23ol7yDuJs85Lc4ARzyoay+b3zOhAKkEMU7chk0xi2hT2OnRP23QUudNNDPTGozX268RGYLexUa6P4xw==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-17.3.0.tgz", + "integrity": "sha512-72K4SeDj3WmehiQqVeOS+icvcO5+JHXK12ee3AqbZGqNqgCKdU4zJRKeC7EGMV4lQhoJXbj8OEdppBLa3qFDhw==", "engines": { "node": ">=8" }, @@ -39071,9 +39071,9 @@ "integrity": "sha512-wvF1PYjyxKNTr6+5w4uR5Gkz53t1fsRDgKjWxDKk7wmlh0cwiILBo4dDFjjVhWRF1mBSjaIxxJGB4WGaP7ct2Q==" }, "@primer/octicons-react": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-16.1.1.tgz", - "integrity": "sha512-xCxQ5z23ol7yDuJs85Lc4ARzyoay+b3zOhAKkEMU7chk0xi2hT2OnRP23QUudNNDPTGozX268RGYLexUa6P4xw==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-17.3.0.tgz", + "integrity": "sha512-72K4SeDj3WmehiQqVeOS+icvcO5+JHXK12ee3AqbZGqNqgCKdU4zJRKeC7EGMV4lQhoJXbj8OEdppBLa3qFDhw==", "requires": {} }, "@primer/primitives": { diff --git a/package.json b/package.json index f77a2ffea2d..529ac237845 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ }, "dependencies": { "@primer/behaviors": "^1.1.1", - "@primer/octicons-react": "^16.1.1", + "@primer/octicons-react": "^17.3.0", "@primer/primitives": "^7.6.0", "@radix-ui/react-polymorphic": "^0.0.14", "@react-aria/ssr": "^3.1.0", From 74e1d1386bc6bb6326c3c2b64b5e31146f9cc56b Mon Sep 17 00:00:00 2001 From: Cole Bemis Date: Wed, 8 Jun 2022 17:12:32 -0700 Subject: [PATCH 04/17] Export NavList from the main bundle (#2112) * Export ActionListDividerProps * Export NavList from the main bundle * Add NavList link to the nav * Create stale-hounds-notice.md * Add componentId --- .changeset/stale-hounds-notice.md | 9 +++++++++ docs/content/NavList.mdx | 12 +++++++----- docs/src/@primer/gatsby-theme-doctocat/nav.yml | 2 ++ src/ActionList/Divider.tsx | 5 +++-- src/ActionList/index.ts | 1 + src/NavList/NavList.tsx | 17 ++++++++++++++--- src/index.ts | 11 +++++++++++ 7 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 .changeset/stale-hounds-notice.md diff --git a/.changeset/stale-hounds-notice.md b/.changeset/stale-hounds-notice.md new file mode 100644 index 00000000000..7727ac53be6 --- /dev/null +++ b/.changeset/stale-hounds-notice.md @@ -0,0 +1,9 @@ +--- +"@primer/react": minor +--- + +[NavList](https://primer.style/NavList) is ready to use. You can now import it from the main bundle: + +```js +import {NavList} from '@primer/react' +``` diff --git a/docs/content/NavList.mdx b/docs/content/NavList.mdx index 9293821a83f..61f2cd0bf8f 100644 --- a/docs/content/NavList.mdx +++ b/docs/content/NavList.mdx @@ -1,12 +1,14 @@ --- title: NavList -status: Draft -description: Use nav list to render a vertical list of navigation links. +status: Alpha +componentId: nav_list +description: Use a nav list to render a vertical list of navigation links. source: https://github.com/primer/react/tree/main/src/NavList +storybook: '/react/storybook/?path=/story/composite-components-navlist--simple' --- ```js -import {NavList} from '@primer/react/drafts' +import {NavList} from '@primer/react' ``` ## Examples @@ -333,9 +335,9 @@ function App() { adaptsToThemes: true, adaptsToScreenSizes: true, fullTestCoverage: true, - usedInProduction: false, + usedInProduction: true, usageExamplesDocumented: true, - hasStorybookStories: false, + hasStorybookStories: true, designReviewed: false, a11yReviewed: false, stableApi: false, diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml index 5464c178421..b353429d7c5 100644 --- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml +++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml @@ -90,6 +90,8 @@ url: /LabelGroup - title: Link url: /Link + - title: NavList + url: /NavList - title: Overlay url: /Overlay - title: Pagehead diff --git a/src/ActionList/Divider.tsx b/src/ActionList/Divider.tsx index e286448eb42..2d03ef24b8a 100644 --- a/src/ActionList/Divider.tsx +++ b/src/ActionList/Divider.tsx @@ -4,11 +4,12 @@ import {get} from '../constants' import {Theme} from '../ThemeProvider' import {SxProp, merge} from '../sx' +export type ActionListDividerProps = SxProp + /** * Visually separates `Item`s or `Group`s in an `ActionList`. */ - -export const Divider: React.FC = ({sx = {}}) => { +export const Divider: React.FC = ({sx = {}}) => { return ( Date: Fri, 17 Jun 2022 13:10:08 +0200 Subject: [PATCH 05/17] Fix CI for Node 16.15.1 (#2123) * update package-lock for node 16.15.1 * use Node 14 for docs * Use Node 14 for deploy production --- .github/workflows/deploy_preview.yml | 2 +- .github/workflows/deploy_production.yml | 2 +- package-lock.json | 26 ++++++++++++------------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deploy_preview.yml b/.github/workflows/deploy_preview.yml index 055fed548f9..71b12de29a5 100644 --- a/.github/workflows/deploy_preview.yml +++ b/.github/workflows/deploy_preview.yml @@ -13,7 +13,7 @@ jobs: name: Preview uses: primer/.github/.github/workflows/deploy_preview.yml@main with: - node_version: 16 + node_version: 14 install: npm ci && cd docs && npm ci && cd .. build: npm run build:docs:preview output_dir: docs/public diff --git a/.github/workflows/deploy_production.yml b/.github/workflows/deploy_production.yml index 0cd2201dd82..d2266bc8405 100644 --- a/.github/workflows/deploy_production.yml +++ b/.github/workflows/deploy_production.yml @@ -38,7 +38,7 @@ jobs: if: ${{ needs.guard.outputs.should_deploy == 'true' }} uses: primer/.github/.github/workflows/deploy.yml@main with: - node_version: 16 + node_version: 14 install: npm ci && cd docs && npm ci && cd .. build: npm run build:docs output_dir: docs/public diff --git a/package-lock.json b/package-lock.json index f1c1012e40d..0438c7d83ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32074,12 +32074,6 @@ "react-dom": ">= 16.3.0" } }, - "node_modules/styled-components/node_modules/stylis": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", - "dev": true - }, "node_modules/styled-system": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/styled-system/-/styled-system-5.1.5.tgz", @@ -32100,6 +32094,12 @@ "object-assign": "^4.1.1" } }, + "node_modules/stylis": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", + "dev": true + }, "node_modules/stylis-rule-sheet": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", @@ -59244,14 +59244,6 @@ "stylis": "^3.5.0", "stylis-rule-sheet": "^0.0.10", "supports-color": "^5.5.0" - }, - "dependencies": { - "stylis": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", - "dev": true - } } }, "styled-system": { @@ -59274,6 +59266,12 @@ "object-assign": "^4.1.1" } }, + "stylis": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", + "dev": true + }, "stylis-rule-sheet": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", From e36a080470d2f9f0b32129b07ec4f6558cf6ee77 Mon Sep 17 00:00:00 2001 From: GitHub Design Systems Bot <30705008+primer-css@users.noreply.github.com> Date: Mon, 20 Jun 2022 02:13:43 -0700 Subject: [PATCH 06/17] Version Packages (#2097) Co-authored-by: github-actions[bot] --- .changeset/funny-hats-sing.md | 10 ---------- .changeset/proud-colts-attend.md | 5 ----- .changeset/slimy-rabbits-try.md | 5 ----- .changeset/stale-hounds-notice.md | 9 --------- CHANGELOG.md | 23 +++++++++++++++++++++++ package.json | 2 +- 6 files changed, 24 insertions(+), 30 deletions(-) delete mode 100644 .changeset/funny-hats-sing.md delete mode 100644 .changeset/proud-colts-attend.md delete mode 100644 .changeset/slimy-rabbits-try.md delete mode 100644 .changeset/stale-hounds-notice.md diff --git a/.changeset/funny-hats-sing.md b/.changeset/funny-hats-sing.md deleted file mode 100644 index 8d14afdcba5..00000000000 --- a/.changeset/funny-hats-sing.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@primer/react": patch ---- - -Export new Dialog component from the `@primer/react/drafts` bundle: - -```diff -- import {Dialog} from '@primer/react/lib-esm/Dialog/Dialog' -+ import {Dialog} from '@primer/react/drafts' -``` diff --git a/.changeset/proud-colts-attend.md b/.changeset/proud-colts-attend.md deleted file mode 100644 index bd60cd15265..00000000000 --- a/.changeset/proud-colts-attend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@primer/react": patch ---- - -Allow minor version updates for production dependencies diff --git a/.changeset/slimy-rabbits-try.md b/.changeset/slimy-rabbits-try.md deleted file mode 100644 index 218eb09fe17..00000000000 --- a/.changeset/slimy-rabbits-try.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': patch ---- - -Communicate the SelectPanel multi-select capability to assistive technologies. diff --git a/.changeset/stale-hounds-notice.md b/.changeset/stale-hounds-notice.md deleted file mode 100644 index 7727ac53be6..00000000000 --- a/.changeset/stale-hounds-notice.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@primer/react": minor ---- - -[NavList](https://primer.style/NavList) is ready to use. You can now import it from the main bundle: - -```js -import {NavList} from '@primer/react' -``` diff --git a/CHANGELOG.md b/CHANGELOG.md index c00c59d729a..acadb864950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # @primer/components +## 35.3.0 + +### Minor Changes + +- [#2112](https://github.com/primer/react/pull/2112) [`74e1d138`](https://github.com/primer/react/commit/74e1d1386bc6bb6326c3c2b64b5e31146f9cc56b) Thanks [@colebemis](https://github.com/colebemis)! - [NavList](https://primer.style/NavList) is ready to use. You can now import it from the main bundle: + + ```js + import {NavList} from '@primer/react' + ``` + +### Patch Changes + +- [#2083](https://github.com/primer/react/pull/2083) [`ea69ccd6`](https://github.com/primer/react/commit/ea69ccd6b5255e70251889ffc2434e975a9c8184) Thanks [@ty-v1](https://github.com/ty-v1)! - Export new Dialog component from the `@primer/react/drafts` bundle: + + ```diff + - import {Dialog} from '@primer/react/lib-esm/Dialog/Dialog' + + import {Dialog} from '@primer/react/drafts' + ``` + +* [#2117](https://github.com/primer/react/pull/2117) [`6e3532cf`](https://github.com/primer/react/commit/6e3532cf8ca11f6edc08e101d9cea4df6a1655ce) Thanks [@dgreif](https://github.com/dgreif)! - Allow minor version updates for production dependencies + +- [#2095](https://github.com/primer/react/pull/2095) [`db5e629c`](https://github.com/primer/react/commit/db5e629c667203728d4256d4b6b549b9d3962e9d) Thanks [@hectahertz](https://github.com/hectahertz)! - Communicate the SelectPanel multi-select capability to assistive technologies. + ## 35.2.2 ### Patch Changes diff --git a/package.json b/package.json index 529ac237845..4b419c4bd13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@primer/react", - "version": "35.2.2", + "version": "35.3.0", "description": "An implementation of GitHub's Primer Design System using React", "main": "lib/index.js", "module": "lib-esm/index.js", From 9b961735f44ee175e2cfd8f3150f8012591fed46 Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Mon, 20 Jun 2022 11:32:41 +0200 Subject: [PATCH 07/17] ADR: Parallel drafts track & plan for deprecated components (#1722) * propose migration plan * Update adr-005.md * Update adr-005.md * move SelectMenu to the first list * Update with parallel track talk * Update adr-005.md * rename back to drafts * Update contributor-docs/adrs/adr-005.md Co-authored-by: Cole Bemis * Apply suggestions from code review Co-authored-by: Cole Bemis Co-authored-by: Leslie Cohn-Wein * Remove component list from ADR Co-authored-by: Mike Perrotti Co-authored-by: Cole Bemis Co-authored-by: Leslie Cohn-Wein --- contributor-docs/adrs/adr-005.md | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 contributor-docs/adrs/adr-005.md diff --git a/contributor-docs/adrs/adr-005.md b/contributor-docs/adrs/adr-005.md new file mode 100644 index 00000000000..52c814d2219 --- /dev/null +++ b/contributor-docs/adrs/adr-005.md @@ -0,0 +1,46 @@ +# ADR 5: Parallel experimental track & plan for deprecated components + +## Status + +Proposed + +## Context + +As we work on maturity of our components, we will sometimes need to build components in a parallel track/bundle without breaking existing components. Eventually, the new components would replace the old ones in the main bundle. + +## Here are the 3 proposed stages: + +### Stage 1 + +Start new component outside the main bundle. Folks who want to try it, need to explicitly import it from the `drafts` bundle. + +`import { ActionMenu } from '@primer/react/drafts'` + +The contract with consumers is - you are opting into a rewrite of the old component that might not cover all the cases of the old component yet. If you are using both the old and new version of the component in different pages, you are paying some additional bundlesize cost. + +Note: If it is a 1:1 replacement, it's useful to keep the component name the same for consumers. Internally, we would want to call the filename `ActionMenu2.tsx` and call it `ActionMenu v2` in the docs. + +### Stage 2 + +After we have the confidence that this component is better than the old version of it, we swap the components and move the old component out of the main bundle. + +This is a breaking change! We are now officially recommended that consumers start using the new component, but if they'd like to push that effort to the future, we give them an easy way out - + +`import { ActionMenu } from '@primer/react/deprecated'` + +The deprecated component does not accept new features requests. + +Reason: Because we have a single bundle for all components, you can not pick the components you want to upgrade. This can result in additional unrelated work. + +### Stage 3 + +After 3 months of living in the `deprecated` bundle, a component is retired/deleted from the codebase. This is also a breaking change. + +At this point, consumers are expected to plan migration work. + +## Suggested changes: + +We should detangle "drafts" from component lifecycle. + +"Drafts components" should not be collocated with "main bundle" components in the documentation or status page. They should have their own section because they are not recommended as an alternative yet. + From 493dc9958dcff6d206d6ddc457b6a8f32359655b Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Mon, 20 Jun 2022 11:39:07 +0200 Subject: [PATCH 08/17] ADR 004: Strict props or Composite components (#1703) * Add ADR for children as API * add adrs to eslint ignore list * editing phase 1 * add NewButton examples * lol title * Replace accidental Button usage * change title * add renderChild to the example * Apply suggestions from code review Co-authored-by: Leslie Cohn-Wein Co-authored-by: Cole Bemis * Apply suggestions from code review Co-authored-by: Cole Bemis * Add decision * Update contributor-docs/adrs/adr-004-children-as-api.md * clarify ActionMenu example is from legacy version * removed sidenote because it feels like a tangent Co-authored-by: Leslie Cohn-Wein Co-authored-by: Cole Bemis --- .../adrs/adr-004-children-as-api.md | 416 ++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 contributor-docs/adrs/adr-004-children-as-api.md diff --git a/contributor-docs/adrs/adr-004-children-as-api.md b/contributor-docs/adrs/adr-004-children-as-api.md new file mode 100644 index 00000000000..0dff7e8f0e8 --- /dev/null +++ b/contributor-docs/adrs/adr-004-children-as-api.md @@ -0,0 +1,416 @@ +# ADR 004: Strict props or Composite components + +## Status + +Approved 2022-05-10 + +
+ +_Note: Consumer is used multiple times on this page. It refers to the developers consuming the component API and not end users._ + +
+ +## Decision: + + +1. Prefer using children for “content” + +2. For composite components, the API should be decided by how much customisation is available for children. + +For components that have design decisions baked in, should use strict props. For example, the color of the icon inside a Button component is decided by the `variant` prop on the Button. The API does not allow for changing that. + +```jsx + +``` + +On the other hand, if we want consumers to have more control over children, a composite API is the better choice. + +```jsx + + + + + mona + +``` + +## Prefer using children for “content” + +With React, `children` is the out-of-the-box way for putting [phrasing content](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#phrasing_content) inside your component. By using `children` instead of our own custom prop, we can make the API “predictable” for its consumers. + +image + +```jsx +// prefer this +Changes saved! +// over this + +``` + +

+ +Children as an API for content is an open and composable approach. The contract here is that the `Flash` controls the container while the consumer of the component controls how the contents inside look. + +Take this example of composition: + +flash with icon + +```jsx +import {Flash} from '@primer/react' +import {CheckIcon} from '@primer/octicons-react' + +render( + + Changes saved! + +) +``` + +
+ +### Pros of this approach here: + +1. The component is aware of recommended use cases and comes with those design decisions backed-in. For example, using an `Icon` with `Flash` is a recognised use case. We don’t lock-in a specific icons, but we do set the size, variant-compatible color and the margin between the icon and text. + + For example: + + flash variants + + ```jsx + + Changes saved! + + + Your changes were not saved! + + ``` + +2. You can bring your own icon components, the component does not depend on a specific version of octicons. +3. When a product team wants to explore a use cases which isn’t baked into the component, they are not blocked by our team. + + Example: + + flash with icon and close + + ```jsx + + + Changes saved! + + + +``` + +```jsx +// we prefer this: + +// over these: + + + + + +
+ +--- + +
+ +### 2. Exposing customisation options for internal components: + +Another place where composite patterns lead to aesthetic predictable APIs is when we want to expose customisation options for internal components. + +For Example, [legacy ActionMenu](https://primer.style/react/deprecated/ActionMenu) accepted `overlayProps` and `anchorContent` to pass it down to the implementation details: + +image 10 + +```jsx + + +``` + +
+ +When we see a a prop which resembles “childProps" or `renderChild` on the container/parent, it's a sign that we should surface this detail in the API by creating a composite component: + +```jsx +// we created an additional layer so that +// the overlay props go on the overlay component + + Open column menu + + ... + + +``` + +
+ +--- + +
+ +### 3. Layout components with unstructured content + +In components where there is a place for consumers to fill in freeform or unstructured content, we should prefer the composite children components. This is especially common in the cases of Dialogs, Menus, Groups. + +Consider this fake `Flash` example where description is unstructured content: + +image 11 + +```jsx +// prefer this: + + Changes saved + + These changes will be applied to your next build. Learn more about builds. + + + +// over this: +// Trying to systemise content by finding patterns in +// unconstructured content can lead to overly prescriptive API +// that is not prectictable and hard to remember + +``` + +
+ +_Sidenote: It’s tempting to change `icon` to `Flash.Icon` here so that it’s consistent with the rest of the contents. This is a purely aesthetic optional choice here:_ + +```jsx + + + Changes saved + + These changes will be applied to your next build. Learn more about builds. + + +``` + +
+ +--- + +
+ +We use this pattern in `ActionList` : + +actionlist + +```jsx + + + + mona + Monalisa Octocat + + + + primer-css + GitHub + + +``` + +--- + +
+ +### Case study with Button: + +image 12 + +Prefer using children for “content” + +```jsx +// we prefer: + +``` + +But, we want to discourage customising the Icon’s color and size in the application. So, in the spirit of making the right thing easy and the wrong thing hard, we ask for the component in a prop instead: + +```jsx +// we prefer: + +// over these: + + +``` + + +image 14 + + +We want to add a `Counter` that adapts to the variant without supporting all the props of a `CounterLabel` like `scheme`. + +`Button.Counter` is a restricted version of `CounterLabel`, making the right thing easy and wrong thing hard: + +```jsx +// we prefer: + +// over this: + + +// it's possible to make a strong case for this option as well: + +``` From 4f6e1596b34237d48552f5ad659d204a2cc3e9c3 Mon Sep 17 00:00:00 2001 From: "Karim K. Kanji" Date: Mon, 20 Jun 2022 14:38:31 +0300 Subject: [PATCH 09/17] Fixed an issue with ButtonDanger Not defined. (#2128) Replaced ButtonDanger with Button Component with the props variant="danger" Co-authored-by: Siddharth Kshetrapal --- docs/content/Details.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/Details.md b/docs/content/Details.md index 8db846589ab..12b9577e37e 100644 --- a/docs/content/Details.md +++ b/docs/content/Details.md @@ -52,7 +52,7 @@ You can also manually show/hide the content using the `setOpen` function returne
Are you sure? - setOpen(false)}>Yes I'm sure +
) }} From 53713b2f3ab7dd7084ce3e602c01c3f66ccd7579 Mon Sep 17 00:00:00 2001 From: Cole Bemis Date: Mon, 20 Jun 2022 10:20:09 -0500 Subject: [PATCH 10/17] Deprecate SideNav in favor of NavList (#2120) * Deprecate SideNav * Create small-donkeys-provide.md --- .changeset/small-donkeys-provide.md | 29 +++++++++++++++++ docs/content/{ => deprecated}/SideNav.md | 32 +++++++++++++++++-- .../src/@primer/gatsby-theme-doctocat/nav.yml | 5 +-- src/SideNav.tsx | 1 + 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 .changeset/small-donkeys-provide.md rename docs/content/{ => deprecated}/SideNav.md (90%) diff --git a/.changeset/small-donkeys-provide.md b/.changeset/small-donkeys-provide.md new file mode 100644 index 00000000000..e8d8e02c13d --- /dev/null +++ b/.changeset/small-donkeys-provide.md @@ -0,0 +1,29 @@ +--- +"@primer/react": patch +--- + +Deprecate SideNav in favor of [NavList](https://primer.style/NavList). + +## Before + +```jsx + + + Home + + About + Contact + +``` + +## After + +```jsx + + + Home + + About + Contact + +``` diff --git a/docs/content/SideNav.md b/docs/content/deprecated/SideNav.md similarity index 90% rename from docs/content/SideNav.md rename to docs/content/deprecated/SideNav.md index 04c2ff0535d..a2db1858682 100644 --- a/docs/content/SideNav.md +++ b/docs/content/deprecated/SideNav.md @@ -1,10 +1,38 @@ --- componentId: side_nav title: SideNav -status: Alpha +status: Deprecated --- -The Side Nav is a vertical list of navigational links, typically used on the left side of a page. For maximum flexibility, **SideNav elements have no default width or positioning.** +The Side Nav is a vertical list of navigational links, typically used on the left side of a page. For maximum flexibility, SideNav elements have no default width or positioning. + +## Deprecation + +Use [NavList](/NavList) instead. + +**Before** + +```jsx + + + Home + + About + Contact + +``` + +**After** + +```jsx + + + Home + + About + Contact + +``` ## Default example diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml index b353429d7c5..48faae7fe91 100644 --- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml +++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml @@ -116,8 +116,7 @@ url: /Select - title: SelectPanel url: /SelectPanel - - title: SideNav - url: /SideNav + - title: Spinner url: /Spinner - title: StateLabel @@ -184,3 +183,5 @@ url: /deprecated/Position - title: SelectMenu url: /deprecated/SelectMenu + - title: SideNav + url: /deprecated/SideNav diff --git a/src/SideNav.tsx b/src/SideNav.tsx index 3b4cc0bd768..2ece1f38816 100644 --- a/src/SideNav.tsx +++ b/src/SideNav.tsx @@ -187,4 +187,5 @@ SideNavLink.displayName = 'SideNav.Link' export type SideNavProps = ComponentProps export type SideNavLinkProps = ComponentProps +/** @deprecated Use [NavList](https://primer.style/react/NavList) instead */ export default Object.assign(SideNav, {Link: SideNavLink}) From 78dc8134b1d38c6826766f2f85ae943e8b1a8088 Mon Sep 17 00:00:00 2001 From: Owen Niblock Date: Tue, 21 Jun 2022 11:34:39 +0100 Subject: [PATCH 11/17] Enforce correct semantics for TabNav (#2125) * Swaps nav and div and adds semantically correct roles * Fix tests, add body div and move sx * Adds changeset * Fixing TabNavProps export Co-authored-by: Siddharth Kshetrapal --- .changeset/poor-wombats-lick.md | 5 +++++ src/TabNav.tsx | 16 +++++++++----- .../__snapshots__/TabNav.test.tsx.snap | 22 ++++++++++++------- 3 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 .changeset/poor-wombats-lick.md diff --git a/.changeset/poor-wombats-lick.md b/.changeset/poor-wombats-lick.md new file mode 100644 index 00000000000..d244a2d551b --- /dev/null +++ b/.changeset/poor-wombats-lick.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Adds roles of tablist and tab to the TabNav component, required rearranging the HTML elements to be semantically correct diff --git a/src/TabNav.tsx b/src/TabNav.tsx index dc7ff810571..fc6cd810a83 100644 --- a/src/TabNav.tsx +++ b/src/TabNav.tsx @@ -11,23 +11,28 @@ const ITEM_CLASS = 'TabNav-item' const SELECTED_CLASS = 'selected' const TabNavBase = styled.div` - margin-top: 0; - border-bottom: 1px solid ${get('colors.border.default')}; ${sx} ` -const TabNavBody = styled.nav` +const TabNavTabList = styled.div` display: flex; margin-bottom: -1px; overflow: auto; ` +const TabNavNav = styled.nav` + margin-top: 0; + border-bottom: 1px solid ${get('colors.border.default')}; +` + export type TabNavProps = ComponentProps function TabNav({children, 'aria-label': ariaLabel, ...rest}: TabNavProps) { return ( - {children} + + {children} + ) } @@ -39,7 +44,8 @@ type StyledTabNavLinkProps = { const TabNavLink = styled.a.attrs(props => ({ activeClassName: typeof props.to === 'string' ? 'selected' : '', - className: classnames(ITEM_CLASS, props.selected && SELECTED_CLASS, props.className) + className: classnames(ITEM_CLASS, props.selected && SELECTED_CLASS, props.className), + role: 'tab' }))` padding: 8px 12px; font-size: ${get('fontSizes.1')}; diff --git a/src/__tests__/__snapshots__/TabNav.test.tsx.snap b/src/__tests__/__snapshots__/TabNav.test.tsx.snap index b912c785a1a..edcc428b949 100644 --- a/src/__tests__/__snapshots__/TabNav.test.tsx.snap +++ b/src/__tests__/__snapshots__/TabNav.test.tsx.snap @@ -46,15 +46,11 @@ exports[`TabNav TabNav.Link renders consistently 1`] = ` `; exports[`TabNav renders consistently 1`] = ` -.c0 { - margin-top: 0; - border-bottom: 1px solid #d0d7de; -} - .c1 { display: -webkit-box; display: -webkit-flex; @@ -64,11 +60,21 @@ exports[`TabNav renders consistently 1`] = ` overflow: auto; } +.c0 { + margin-top: 0; + border-bottom: 1px solid #d0d7de; +} +
`; From c7abeb3a6bde7ac13983d1518b78831d35718a41 Mon Sep 17 00:00:00 2001 From: Jon Rohan Date: Tue, 21 Jun 2022 04:15:10 -0700 Subject: [PATCH 12/17] Adding codeql scanning (#2134) https://github.com/github/primer/issues/937 --- .github/workflows/codeql.yml | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..d935db63df7 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '18 23 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript', 'ruby' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 From 65fcd9f23de939014351f8e135f912cfa00f71a3 Mon Sep 17 00:00:00 2001 From: Matthew Costabile Date: Tue, 21 Jun 2022 12:35:13 -0400 Subject: [PATCH 13/17] Navlist passthrough action list group props (#2133) * passthrough actionlist group props to navlist.group * changeset * pass through props * fix pass through block * Update docs/content/NavList.mdx * avoid passing through action list props, but spread additional props to allow aria and data attributes through Co-authored-by: Cole Bemis --- .changeset/smooth-balloons-hope.md | 5 +++++ src/NavList/NavList.tsx | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/smooth-balloons-hope.md diff --git a/.changeset/smooth-balloons-hope.md b/.changeset/smooth-balloons-hope.md new file mode 100644 index 00000000000..e9229a3c876 --- /dev/null +++ b/.changeset/smooth-balloons-hope.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Passthrough ActionList.Group props from NavList.Group diff --git a/src/NavList/NavList.tsx b/src/NavList/NavList.tsx index 798e39b82a3..40386ad7f2a 100644 --- a/src/NavList/NavList.tsx +++ b/src/NavList/NavList.tsx @@ -240,13 +240,14 @@ export type NavListGroupProps = { title?: string } & SxProp +const defaultSx = {} // TODO: ref prop -const Group = ({title, children, sx: sxProp = {}}: NavListGroupProps) => { +const Group: React.VFC = ({title, children, sx: sxProp = defaultSx, ...props}) => { return ( <> {/* Hide divider if the group is the first item in the list */} - + {children} From df26f3c3aac7cdb5f37f956e2f950522e7bdc732 Mon Sep 17 00:00:00 2001 From: Rez Date: Tue, 21 Jun 2022 23:41:11 +0100 Subject: [PATCH 14/17] Explicitly declare workflow permissions for previews (#2137) * explicitly declare workflow permissions for previews --- .github/workflows/deploy_preview.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy_preview.yml b/.github/workflows/deploy_preview.yml index 71b12de29a5..4cc167fd2dd 100644 --- a/.github/workflows/deploy_preview.yml +++ b/.github/workflows/deploy_preview.yml @@ -12,6 +12,10 @@ jobs: if: ${{ github.repository == 'primer/react' }} name: Preview uses: primer/.github/.github/workflows/deploy_preview.yml@main + permissions: + contents: read + pages: write + id-token: write with: node_version: 14 install: npm ci && cd docs && npm ci && cd .. From e5be3db3112db20efef5e49ebe89ea3af17fd486 Mon Sep 17 00:00:00 2001 From: Mike Perrotti Date: Thu, 23 Jun 2022 16:02:01 -0500 Subject: [PATCH 15/17] Basic SegmentedControl functionality (#2108) * implements basic SegmentedControl functionality * updates file structure * adds SegmentedControl to drafts * adds changeset * fixes TypeScripts issues * revert package-lock.json changes * fixes SegmentedControl tests and updates snapshot * style bug fixes * Update src/SegmentedControl/fixtures.stories.tsx Co-authored-by: Siddharth Kshetrapal * improve visual design for hover and active states * ARIA updates from Chelsea's feedback * updates tests and snapshots * Ignore *.test.tsx files in build types * Use named export for SegmentedControl This fixes live code examples in the docs * Update package-lock.json * updates lock file * fixes checkExports test for SegmentedControl * design tweak for icon-only segmented control button Co-authored-by: Siddharth Kshetrapal Co-authored-by: Cole Bemis --- .changeset/modern-fireants-destroy.md | 5 + docs/content/SegmentedControl.mdx | 11 +- .../SegmentedControl.test.tsx | 139 +++++++ src/SegmentedControl/SegmentedControl.tsx | 77 ++++ .../SegmentedControlButton.tsx | 44 ++ .../SegmentedControlIconButton.tsx | 40 ++ .../SegmentedControl.test.tsx.snap | 381 ++++++++++++++++++ src/SegmentedControl/examples.stories.tsx | 83 ++++ src/SegmentedControl/fixtures.stories.tsx | 50 +++ .../getSegmentedControlStyles.ts | 104 +++++ src/SegmentedControl/index.ts | 1 + src/drafts/index.ts | 1 + src/utils/testing.tsx | 4 +- tsconfig.build.json | 9 +- 14 files changed, 943 insertions(+), 6 deletions(-) create mode 100644 .changeset/modern-fireants-destroy.md create mode 100644 src/SegmentedControl/SegmentedControl.test.tsx create mode 100644 src/SegmentedControl/SegmentedControl.tsx create mode 100644 src/SegmentedControl/SegmentedControlButton.tsx create mode 100644 src/SegmentedControl/SegmentedControlIconButton.tsx create mode 100644 src/SegmentedControl/__snapshots__/SegmentedControl.test.tsx.snap create mode 100644 src/SegmentedControl/examples.stories.tsx create mode 100644 src/SegmentedControl/fixtures.stories.tsx create mode 100644 src/SegmentedControl/getSegmentedControlStyles.ts create mode 100644 src/SegmentedControl/index.ts diff --git a/.changeset/modern-fireants-destroy.md b/.changeset/modern-fireants-destroy.md new file mode 100644 index 00000000000..2db0246c301 --- /dev/null +++ b/.changeset/modern-fireants-destroy.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Adds a draft component to render a basic segmented control. diff --git a/docs/content/SegmentedControl.mdx b/docs/content/SegmentedControl.mdx index 4da191647e2..65b2b220701 100644 --- a/docs/content/SegmentedControl.mdx +++ b/docs/content/SegmentedControl.mdx @@ -157,6 +157,7 @@ description: Use a segmented control to let users select an option from a short name="onChange" type="(selectedIndex?: number) => void" description="The handler that gets called when a segment is selected" + required /> - @@ -184,8 +184,13 @@ description: Use a segmented control to let users select an option from a short ### SegmentedControl.IconButton - - + + diff --git a/src/SegmentedControl/SegmentedControl.test.tsx b/src/SegmentedControl/SegmentedControl.test.tsx new file mode 100644 index 00000000000..30f574e984f --- /dev/null +++ b/src/SegmentedControl/SegmentedControl.test.tsx @@ -0,0 +1,139 @@ +import React from 'react' +import '@testing-library/jest-dom/extend-expect' +import {render} from '@testing-library/react' +import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react' +import userEvent from '@testing-library/user-event' +import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing' +import {SegmentedControl} from '.' // TODO: update import when we move this to the global index + +const segmentData = [ + {label: 'Preview', iconLabel: 'EyeIcon', icon: () => }, + {label: 'Raw', iconLabel: 'FileCodeIcon', icon: () => }, + {label: 'Blame', iconLabel: 'PeopleIcon', icon: () => } +] + +// TODO: improve test coverage +describe('SegmentedControl', () => { + behavesAsComponent({ + Component: SegmentedControl, + toRender: () => ( + + Preview + Raw + Blame + + ) + }) + + checkExports('SegmentedControl', { + default: undefined, + SegmentedControl + }) + + it('renders with a selected segment', () => { + const {getByText} = render( + + {segmentData.map(({label}, index) => ( + + {label} + + ))} + + ) + + const selectedButton = getByText('Raw').closest('button') + + expect(selectedButton?.getAttribute('aria-current')).toBe('true') + }) + + it('renders the first segment as selected if no child has the `selected` prop passed', () => { + const {getByText} = render( + + {segmentData.map(({label}) => ( + {label} + ))} + + ) + + const selectedButton = getByText('Preview').closest('button') + + expect(selectedButton?.getAttribute('aria-current')).toBe('true') + }) + + it('renders segments with segment labels that have leading icons', () => { + const {getByLabelText} = render( + + {segmentData.map(({label, icon}, index) => ( + + {label} + + ))} + + ) + + for (const datum of segmentData) { + const iconEl = getByLabelText(datum.iconLabel) + expect(iconEl).toBeDefined() + } + }) + + it('renders segments with accessible icon-only labels', () => { + const {getByLabelText} = render( + + {segmentData.map(({label, icon}) => ( + + ))} + + ) + + for (const datum of segmentData) { + const labelledButton = getByLabelText(datum.label) + expect(labelledButton).toBeDefined() + } + }) + + it('calls onChange with index of clicked segment button', () => { + const handleChange = jest.fn() + const {getByText} = render( + + {segmentData.map(({label}, index) => ( + + {label} + + ))} + + ) + + const buttonToClick = getByText('Raw').closest('button') + + expect(handleChange).not.toHaveBeenCalled() + if (buttonToClick) { + userEvent.click(buttonToClick) + } + expect(handleChange).toHaveBeenCalledWith(1) + }) + + it('calls segment button onClick if it is passed', () => { + const handleClick = jest.fn() + const {getByText} = render( + + {segmentData.map(({label}, index) => ( + + {label} + + ))} + + ) + + const buttonToClick = getByText('Raw').closest('button') + + expect(handleClick).not.toHaveBeenCalled() + if (buttonToClick) { + userEvent.click(buttonToClick) + } + expect(handleClick).toHaveBeenCalled() + }) +}) + +checkStoriesForAxeViolations('examples', '../SegmentedControl/') +checkStoriesForAxeViolations('fixtures', '../SegmentedControl/') diff --git a/src/SegmentedControl/SegmentedControl.tsx b/src/SegmentedControl/SegmentedControl.tsx new file mode 100644 index 00000000000..efb9767e0a2 --- /dev/null +++ b/src/SegmentedControl/SegmentedControl.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import Button, {SegmentedControlButtonProps} from './SegmentedControlButton' +import SegmentedControlIconButton, {SegmentedControlIconButtonProps} from './SegmentedControlIconButton' +import {Box, useTheme} from '..' +import {merge, SxProp} from '../sx' + +type SegmentedControlProps = { + 'aria-label'?: string + 'aria-labelledby'?: string + 'aria-describedby'?: string + /** Whether the control fills the width of its parent */ + fullWidth?: boolean + /** The handler that gets called when a segment is selected */ + onChange?: (selectedIndex: number) => void // TODO: consider making onChange required if we force this component to be controlled +} & SxProp + +const getSegmentedControlStyles = (props?: SegmentedControlProps) => ({ + // TODO: update color primitive name(s) to use different primitives: + // - try to use general 'control' primitives (e.g.: https://primer.style/primitives/spacing#ui-control) + // - when that's not possible, use specific to segmented controls + backgroundColor: 'switchTrack.bg', // TODO: update primitive when it is available + borderColor: 'border.default', + borderRadius: 2, + borderStyle: 'solid', + borderWidth: 1, + display: props?.fullWidth ? 'flex' : 'inline-flex', + height: '32px' // TODO: use primitive `primer.control.medium.size` when it is available +}) + +// TODO: implement `variant` prop for responsive behavior +// TODO: implement `loading` prop +// TODO: log a warning if no `ariaLabel` or `ariaLabelledBy` prop is passed +// TODO: implement keyboard behavior to move focus using the arrow keys +const Root: React.FC = ({children, fullWidth, onChange, sx: sxProp = {}, ...rest}) => { + const {theme} = useTheme() + const selectedChildren = React.Children.toArray(children).map( + child => + React.isValidElement(child) && child.props.selected + ) + const hasSelectedButton = selectedChildren.some(isSelected => isSelected) + const selectedIndex = hasSelectedButton ? selectedChildren.indexOf(true) : 0 + const sx = merge( + getSegmentedControlStyles({ + fullWidth + }), + sxProp as SxProp + ) + + return ( + + {React.Children.map(children, (child, i) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + onClick: onChange + ? (e: React.MouseEvent) => { + onChange(i) + child.props.onClick && child.props.onClick(e) + } + : child.props.onClick, + selected: i === selectedIndex, + sx: { + '--separator-color': + i === selectedIndex || i === selectedIndex - 1 ? 'transparent' : theme?.colors.border.default + } as React.CSSProperties + }) + } + })} + + ) +} + +Root.displayName = 'SegmentedControl' + +export const SegmentedControl = Object.assign(Root, { + Button, + IconButton: SegmentedControlIconButton +}) diff --git a/src/SegmentedControl/SegmentedControlButton.tsx b/src/SegmentedControl/SegmentedControlButton.tsx new file mode 100644 index 00000000000..7aacd5cb4d4 --- /dev/null +++ b/src/SegmentedControl/SegmentedControlButton.tsx @@ -0,0 +1,44 @@ +import React, {HTMLAttributes} from 'react' +import {IconProps} from '@primer/octicons-react' +import styled from 'styled-components' +import {Box} from '..' +import sx, {merge, SxProp} from '../sx' +import getSegmentedControlButtonStyles from './getSegmentedControlStyles' + +export type SegmentedControlButtonProps = { + children?: string + /** Whether the segment is selected */ + selected?: boolean + /** The leading icon comes before item label */ + leadingIcon?: React.FunctionComponent +} & SxProp & + HTMLAttributes + +const SegmentedControlButtonStyled = styled.button` + ${sx}; +` + +const SegmentedControlButton: React.FC = ({ + children, + leadingIcon: LeadingIcon, + selected, + sx: sxProp = {}, + ...rest +}) => { + const mergedSx = merge(getSegmentedControlButtonStyles({selected, children}), sxProp as SxProp) + + return ( + + + {LeadingIcon && ( + + + + )} + {children} + + + ) +} + +export default SegmentedControlButton diff --git a/src/SegmentedControl/SegmentedControlIconButton.tsx b/src/SegmentedControl/SegmentedControlIconButton.tsx new file mode 100644 index 00000000000..3f6f9f0f075 --- /dev/null +++ b/src/SegmentedControl/SegmentedControlIconButton.tsx @@ -0,0 +1,40 @@ +import React, {HTMLAttributes} from 'react' +import {IconProps} from '@primer/octicons-react' +import styled from 'styled-components' +import sx, {merge, SxProp} from '../sx' +import getSegmentedControlButtonStyles from './getSegmentedControlStyles' + +export type SegmentedControlIconButtonProps = { + 'aria-label': string + /** The icon that represents the segmented control item */ + icon: React.FunctionComponent + /** Whether the segment is selected */ + selected?: boolean +} & SxProp & + HTMLAttributes + +const SegmentedControlIconButtonStyled = styled.button` + ${sx}; +` + +// TODO: get tooltips working: +// - by default, the tooltip shows the `ariaLabel` content +// - allow users to pass custom tooltip text +export const SegmentedControlIconButton: React.FC = ({ + icon: Icon, + selected, + sx: sxProp = {}, + ...rest +}) => { + const mergedSx = merge(getSegmentedControlButtonStyles({selected, isIconOnly: true}), sxProp as SxProp) + + return ( + + + + + + ) +} + +export default SegmentedControlIconButton diff --git a/src/SegmentedControl/__snapshots__/SegmentedControl.test.tsx.snap b/src/SegmentedControl/__snapshots__/SegmentedControl.test.tsx.snap new file mode 100644 index 00000000000..997d12c64d9 --- /dev/null +++ b/src/SegmentedControl/__snapshots__/SegmentedControl.test.tsx.snap @@ -0,0 +1,381 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SegmentedControl renders consistently 1`] = ` +.c0 { + background-color: #eaeef2; + border-color: #d0d7de; + border-radius: 6px; + border-style: solid; + border-width: 1px; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + height: 32px; +} + +.c1 { + --segmented-control-button-inner-padding: 12px; + --segmented-control-button-bg-inset: 4px; + --segmented-control-outer-radius: 6px; + background-color: transparent; + border-color: transparent; + border-radius: var(--segmented-control-outer-radius); + border-width: 0; + color: currentColor; + cursor: pointer; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + font-family: inherit; + font-weight: 600; + margin-top: -1px; + margin-bottom: -1px; + padding: 0; + position: relative; + --separator-color: transparent; +} + +.c1 .segmentedControl-content { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #f6f8fa; + border-color: #8c959f; + border-style: solid; + border-width: 1px; + border-radius: var(--segmented-control-outer-radius); + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + height: 100%; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding-left: var(--segmented-control-button-inner-padding); + padding-right: var(--segmented-control-button-inner-padding); +} + +.c1 svg { + fill: #57606a; +} + +.c1:first-child { + margin-left: -1px; +} + +.c1:last-child { + margin-right: -1px; +} + +.c1:not(:last-child) { + margin-right: 1px; +} + +.c1:not(:last-child):after { + background-color: var(--separator-color); + content: ""; + position: absolute; + right: -2px; + top: 8px; + bottom: 8px; + width: 1px; +} + +.c1 .segmentedControl-text:after { + content: "Preview"; + display: block; + font-weight: 600; + height: 0; + overflow: hidden; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + visibility: hidden; +} + +.c2 { + --segmented-control-button-inner-padding: 12px; + --segmented-control-button-bg-inset: 4px; + --segmented-control-outer-radius: 6px; + background-color: transparent; + border-color: transparent; + border-radius: var(--segmented-control-outer-radius); + border-width: 0; + color: currentColor; + cursor: pointer; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + font-family: inherit; + font-weight: 400; + margin-top: -1px; + margin-bottom: -1px; + padding: var(--segmented-control-button-bg-inset); + position: relative; + --separator-color: #d0d7de; +} + +.c2 .segmentedControl-content { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: transparent; + border-color: transparent; + border-style: solid; + border-width: 1px; + border-radius: calc(var(--segmented-control-outer-radius) - var(--segmented-control-button-bg-inset) / 2); + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + height: 100%; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding-left: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); + padding-right: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); +} + +.c2 svg { + fill: #57606a; +} + +.c2:hover .segmentedControl-content { + background-color: rgba(208,215,222,0.32); +} + +.c2:active .segmentedControl-content { + background-color: rgba(208,215,222,0.48); +} + +.c2:first-child { + margin-left: -1px; +} + +.c2:last-child { + margin-right: -1px; +} + +.c2:not(:last-child) { + margin-right: 1px; +} + +.c2:not(:last-child):after { + background-color: var(--separator-color); + content: ""; + position: absolute; + right: -2px; + top: 8px; + bottom: 8px; + width: 1px; +} + +.c2 .segmentedControl-text:after { + content: "Raw"; + display: block; + font-weight: 600; + height: 0; + overflow: hidden; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + visibility: hidden; +} + +.c3 { + --segmented-control-button-inner-padding: 12px; + --segmented-control-button-bg-inset: 4px; + --segmented-control-outer-radius: 6px; + background-color: transparent; + border-color: transparent; + border-radius: var(--segmented-control-outer-radius); + border-width: 0; + color: currentColor; + cursor: pointer; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + font-family: inherit; + font-weight: 400; + margin-top: -1px; + margin-bottom: -1px; + padding: var(--segmented-control-button-bg-inset); + position: relative; + --separator-color: #d0d7de; +} + +.c3 .segmentedControl-content { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: transparent; + border-color: transparent; + border-style: solid; + border-width: 1px; + border-radius: calc(var(--segmented-control-outer-radius) - var(--segmented-control-button-bg-inset) / 2); + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + height: 100%; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding-left: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); + padding-right: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); +} + +.c3 svg { + fill: #57606a; +} + +.c3:hover .segmentedControl-content { + background-color: rgba(208,215,222,0.32); +} + +.c3:active .segmentedControl-content { + background-color: rgba(208,215,222,0.48); +} + +.c3:first-child { + margin-left: -1px; +} + +.c3:last-child { + margin-right: -1px; +} + +.c3:not(:last-child) { + margin-right: 1px; +} + +.c3:not(:last-child):after { + background-color: var(--separator-color); + content: ""; + position: absolute; + right: -2px; + top: 8px; + bottom: 8px; + width: 1px; +} + +.c3 .segmentedControl-text:after { + content: "Blame"; + display: block; + font-weight: 600; + height: 0; + overflow: hidden; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + visibility: hidden; +} + +@media (pointer:coarse) { + .c1:before { + content: ""; + position: absolute; + left: 0; + right: 0; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + top: 50%; + min-height: 44px; + } +} + +@media (pointer:coarse) { + .c2:before { + content: ""; + position: absolute; + left: 0; + right: 0; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + top: 50%; + min-height: 44px; + } +} + +@media (pointer:coarse) { + .c3:before { + content: ""; + position: absolute; + left: 0; + right: 0; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + top: 50%; + min-height: 44px; + } +} + +
+ + + +
+`; diff --git a/src/SegmentedControl/examples.stories.tsx b/src/SegmentedControl/examples.stories.tsx new file mode 100644 index 00000000000..cef34a873c3 --- /dev/null +++ b/src/SegmentedControl/examples.stories.tsx @@ -0,0 +1,83 @@ +import React, {useState} from 'react' +import {Meta} from '@storybook/react' + +import {BaseStyles, ThemeProvider} from '..' +import {ComponentProps} from '../utils/types' +import {SegmentedControl} from '.' +import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react' + +type Args = ComponentProps + +const excludedControlKeys = ['aria-label', 'onChange', 'sx'] + +export default { + title: 'SegmentedControl/examples', + component: SegmentedControl, + argTypes: { + fullWidth: { + defaultValue: false, + control: { + type: 'boolean' + } + }, + loading: { + defaultValue: false, + control: { + type: 'boolean' + } + } + }, + parameters: {controls: {exclude: excludedControlKeys}}, + decorators: [ + Story => { + return ( + + + + + + ) + } + ] +} as Meta + +export const Default = (args: Args) => ( + + Preview + Raw + Blame + +) + +export const Controlled = (args: Args) => { + const [selectedIndex, setSelectedIndex] = useState(1) + const handleChange = (i: number) => { + setSelectedIndex(i) + } + + return ( + + Preview + Raw + Blame + + ) +} + +export const WithIconsAndLabels = (args: Args) => ( + + + Preview + + Raw + Blame + +) + +export const IconsOnly = (args: Args) => ( + + + + + +) diff --git a/src/SegmentedControl/fixtures.stories.tsx b/src/SegmentedControl/fixtures.stories.tsx new file mode 100644 index 00000000000..888a8d36b20 --- /dev/null +++ b/src/SegmentedControl/fixtures.stories.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import {Meta} from '@storybook/react' + +import {BaseStyles, Box, Text, ThemeProvider} from '../' +import {SegmentedControl} from '.' + +export default { + title: 'SegmentedControl/fixtures', + component: SegmentedControl, + decorators: [ + Story => { + return ( + + + + + + ) + } + ] +} as Meta + +// TODO: make it possible to use FormControl +// - FormControl.Label needs to accept `id` prop +// - FormControl.Label needs to accept a prop that lets it render an element that isn't a `