From 885064edda758f59eb0ce2031632d9924ea6987d Mon Sep 17 00:00:00 2001 From: Cole Bemis Date: Mon, 1 Aug 2022 17:52:44 -0700 Subject: [PATCH 1/3] PageLayout: Add `padding` prop to subcomponents (#2201) * Add a padding prop to PageLayout subcomponents * Create funny-roses-impress.md --- .changeset/funny-roses-impress.md | 5 + docs/content/PageLayout.mdx | 63 ++++- src/PageLayout/PageLayout.tsx | 18 +- .../__snapshots__/PageLayout.test.tsx.snap | 264 +++++++++++------- 4 files changed, 236 insertions(+), 114 deletions(-) create mode 100644 .changeset/funny-roses-impress.md diff --git a/.changeset/funny-roses-impress.md b/.changeset/funny-roses-impress.md new file mode 100644 index 00000000000..16e99ea1919 --- /dev/null +++ b/.changeset/funny-roses-impress.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Add `padding` prop to `PageLayout.Header`, `PageLayout.Content`, `PageLayout.Pane`, and `PageLayout.Footer` diff --git a/docs/content/PageLayout.mdx b/docs/content/PageLayout.mdx index c4f939df260..8511c8951ec 100644 --- a/docs/content/PageLayout.mdx +++ b/docs/content/PageLayout.mdx @@ -58,6 +58,25 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo ``` +### With connected dividers + +```jsx live + + + + + + + + + + + + + + +``` + ### With pane on left ```jsx live @@ -145,23 +164,23 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo @@ -170,6 +189,14 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo ### PageLayout.Header + + + + > = ({varia // PageLayout.Header export type PageLayoutHeaderProps = { + padding?: keyof typeof SPACING_MAP divider?: 'none' | 'line' | ResponsiveValue<'none' | 'line', 'none' | 'line' | 'filled'> /** * @deprecated Use the `divider` prop with a responsive value instead. @@ -191,6 +192,7 @@ export type PageLayoutHeaderProps = { } & SxProp const Header: React.FC> = ({ + padding = 'none', divider = 'none', dividerWhenNarrow = 'inherit', hidden = false, @@ -219,7 +221,7 @@ const Header: React.FC> = ({ sx )} > - {children} + {children} ) @@ -232,6 +234,7 @@ Header.displayName = 'PageLayout.Header' export type PageLayoutContentProps = { width?: keyof typeof contentWidths + padding?: keyof typeof SPACING_MAP hidden?: boolean | ResponsiveValue } & SxProp @@ -245,6 +248,7 @@ const contentWidths = { const Content: React.FC> = ({ width = 'full', + padding = 'none', hidden = false, children, sx = {} @@ -268,7 +272,9 @@ const Content: React.FC> = ({ sx )} > - {children} + + {children} + ) } @@ -296,6 +302,7 @@ export type PageLayoutPaneProps = { */ positionWhenNarrow?: 'inherit' | keyof typeof panePositions width?: keyof typeof paneWidths + padding?: keyof typeof SPACING_MAP divider?: 'none' | 'line' | ResponsiveValue<'none' | 'line', 'none' | 'line' | 'filled'> /** * @deprecated Use the `divider` prop with a responsive value instead. @@ -330,6 +337,7 @@ const Pane: React.FC> = ({ position = 'end', positionWhenNarrow = 'inherit', width = 'medium', + padding = 'none', divider = 'none', dividerWhenNarrow = 'inherit', hidden = false, @@ -388,7 +396,7 @@ const Pane: React.FC> = ({ sx={{[responsivePosition === 'end' ? 'marginRight' : 'marginLeft']: SPACING_MAP[columnGap]}} /> - {children} + {children} ) } @@ -399,6 +407,7 @@ Pane.displayName = 'PageLayout.Pane' // PageLayout.Footer export type PageLayoutFooterProps = { + padding?: keyof typeof SPACING_MAP divider?: 'none' | 'line' | ResponsiveValue<'none' | 'line', 'none' | 'line' | 'filled'> /** * @deprecated Use the `divider` prop with a responsive value instead. @@ -419,6 +428,7 @@ export type PageLayoutFooterProps = { } & SxProp const Footer: React.FC> = ({ + padding = 'none', divider = 'none', dividerWhenNarrow = 'inherit', hidden = false, @@ -448,7 +458,7 @@ const Footer: React.FC> = ({ )} > - {children} + {children} ) } diff --git a/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap b/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap index cb10f8a8e57..5f9e4c680bb 100644 --- a/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap +++ b/src/PageLayout/__snapshots__/PageLayout.test.tsx.snap @@ -14,7 +14,11 @@ exports[`PageLayout renders condensed layout 1`] = ` flex-wrap: wrap; } -.c4 { +.c3 { + padding: 0; +} + +.c5 { -webkit-order: 2; -ms-flex-order: 2; order: 2; @@ -31,15 +35,17 @@ exports[`PageLayout renders condensed layout 1`] = ` min-width: 1px; } -.c5 { +.c6 { width: 100%; max-width: 100%; margin-left: auto; margin-right: auto; + padding: 0; } -.c9 { +.c10 { width: 100%; + padding: 0; } .c0 { @@ -54,14 +60,14 @@ exports[`PageLayout renders condensed layout 1`] = ` margin-bottom: 16px; } -.c3 { +.c4 { margin-left: -16px; margin-right: -16px; display: none; margin-top: 16px; } -.c6 { +.c7 { -webkit-order: 3; -ms-flex-order: 3; order: 3; @@ -78,20 +84,20 @@ exports[`PageLayout renders condensed layout 1`] = ` margin-top: 16px; } -.c7 { +.c8 { margin-left: -16px; margin-right: -16px; display: none; margin-bottom: 16px; } -.c8 { +.c9 { height: 100%; display: none; margin-right: 16px; } -.c10 { +.c11 { -webkit-order: 4; -ms-flex-order: 4; order: 4; @@ -100,26 +106,26 @@ exports[`PageLayout renders condensed layout 1`] = ` } @media screen and (min-width:768px) { - .c9 { + .c10 { width: 256px; } } @media screen and (min-width:1012px) { - .c9 { + .c10 { width: 296px; } } @media screen and (min-width:768px) { - .c3 { + .c4 { margin-left: 0 !important; margin-right: 0 !important; } } @media screen and (min-width:768px) { - .c6 { + .c7 { width: auto; margin-left: 16px; margin-top: 0 !important; @@ -134,7 +140,7 @@ exports[`PageLayout renders condensed layout 1`] = ` } @media screen and (min-width:768px) { - .c7 { + .c8 { margin-left: 0 !important; margin-right: 0 !important; } @@ -150,42 +156,50 @@ exports[`PageLayout renders condensed layout 1`] = `
- Header
+ Header +
+
Content
- Footer +
+ Footer +
@@ -219,13 +233,17 @@ exports[`PageLayout renders default layout 1`] = ` } .c3 { + padding: 0; +} + +.c4 { margin-left: -16px; margin-right: -16px; display: none; margin-top: 16px; } -.c4 { +.c5 { -webkit-order: 2; -ms-flex-order: 2; order: 2; @@ -242,14 +260,15 @@ exports[`PageLayout renders default layout 1`] = ` min-width: 1px; } -.c5 { +.c6 { width: 100%; max-width: 100%; margin-left: auto; margin-right: auto; + padding: 0; } -.c6 { +.c7 { -webkit-order: 3; -ms-flex-order: 3; order: 3; @@ -266,24 +285,25 @@ exports[`PageLayout renders default layout 1`] = ` margin-top: 16px; } -.c7 { +.c8 { margin-left: -16px; margin-right: -16px; display: none; margin-bottom: 16px; } -.c8 { +.c9 { height: 100%; display: none; margin-right: 16px; } -.c9 { +.c10 { width: 100%; + padding: 0; } -.c10 { +.c11 { -webkit-order: 4; -ms-flex-order: 4; order: 4; @@ -304,14 +324,14 @@ exports[`PageLayout renders default layout 1`] = ` } @media screen and (min-width:768px) { - .c3 { + .c4 { margin-left: 0 !important; margin-right: 0 !important; } } @media screen and (min-width:1012px) { - .c3 { + .c4 { margin-left: -24px; margin-right: -24px; margin-top: 24px; @@ -319,7 +339,7 @@ exports[`PageLayout renders default layout 1`] = ` } @media screen and (min-width:768px) { - .c6 { + .c7 { width: auto; margin-left: 16px; margin-top: 0 !important; @@ -334,20 +354,20 @@ exports[`PageLayout renders default layout 1`] = ` } @media screen and (min-width:1012px) { - .c6 { + .c7 { margin-top: 24px; } } @media screen and (min-width:768px) { - .c7 { + .c8 { margin-left: 0 !important; margin-right: 0 !important; } } @media screen and (min-width:1012px) { - .c7 { + .c8 { margin-left: -24px; margin-right: -24px; margin-bottom: 24px; @@ -355,25 +375,25 @@ exports[`PageLayout renders default layout 1`] = ` } @media screen and (min-width:1012px) { - .c8 { + .c9 { margin-right: 24px; } } @media screen and (min-width:768px) { - .c9 { + .c10 { width: 256px; } } @media screen and (min-width:1012px) { - .c9 { + .c10 { width: 296px; } } @media screen and (min-width:1012px) { - .c10 { + .c11 { margin-top: 24px; } } @@ -388,42 +408,50 @@ exports[`PageLayout renders default layout 1`] = `
- Header
+ Header +
+
Content
- Footer +
+ Footer +
@@ -457,13 +485,17 @@ exports[`PageLayout renders pane in different position when narrow 1`] = ` } .c3 { + padding: 0; +} + +.c4 { margin-left: -16px; margin-right: -16px; display: none; margin-top: 16px; } -.c4 { +.c5 { -webkit-order: 2; -ms-flex-order: 2; order: 2; @@ -480,14 +512,15 @@ exports[`PageLayout renders pane in different position when narrow 1`] = ` min-width: 1px; } -.c5 { +.c6 { width: 100%; max-width: 100%; margin-left: auto; margin-right: auto; + padding: 0; } -.c6 { +.c7 { -webkit-order: 3; -ms-flex-order: 3; order: 3; @@ -504,24 +537,25 @@ exports[`PageLayout renders pane in different position when narrow 1`] = ` margin-top: 16px; } -.c7 { +.c8 { margin-left: -16px; margin-right: -16px; display: none; margin-bottom: 16px; } -.c8 { +.c9 { height: 100%; display: none; margin-right: 16px; } -.c9 { +.c10 { width: 100%; + padding: 0; } -.c10 { +.c11 { -webkit-order: 4; -ms-flex-order: 4; order: 4; @@ -542,14 +576,14 @@ exports[`PageLayout renders pane in different position when narrow 1`] = ` } @media screen and (min-width:768px) { - .c3 { + .c4 { margin-left: 0 !important; margin-right: 0 !important; } } @media screen and (min-width:1012px) { - .c3 { + .c4 { margin-left: -24px; margin-right: -24px; margin-top: 24px; @@ -557,7 +591,7 @@ exports[`PageLayout renders pane in different position when narrow 1`] = ` } @media screen and (min-width:768px) { - .c6 { + .c7 { width: auto; margin-left: 16px; margin-top: 0 !important; @@ -572,20 +606,20 @@ exports[`PageLayout renders pane in different position when narrow 1`] = ` } @media screen and (min-width:1012px) { - .c6 { + .c7 { margin-top: 24px; } } @media screen and (min-width:768px) { - .c7 { + .c8 { margin-left: 0 !important; margin-right: 0 !important; } } @media screen and (min-width:1012px) { - .c7 { + .c8 { margin-left: -24px; margin-right: -24px; margin-bottom: 24px; @@ -593,25 +627,25 @@ exports[`PageLayout renders pane in different position when narrow 1`] = ` } @media screen and (min-width:1012px) { - .c8 { + .c9 { margin-right: 24px; } } @media screen and (min-width:768px) { - .c9 { + .c10 { width: 256px; } } @media screen and (min-width:1012px) { - .c9 { + .c10 { width: 296px; } } @media screen and (min-width:1012px) { - .c10 { + .c11 { margin-top: 24px; } } @@ -626,42 +660,50 @@ exports[`PageLayout renders pane in different position when narrow 1`] = `
- Header
+ Header +
+
Content
- Footer +
+ Footer +
@@ -695,13 +737,17 @@ exports[`PageLayout renders with dividers 1`] = ` } .c3 { + padding: 0; +} + +.c4 { margin-left: -16px; margin-right: -16px; display: none; margin-top: 16px; } -.c4 { +.c5 { -webkit-order: 2; -ms-flex-order: 2; order: 2; @@ -718,25 +764,27 @@ exports[`PageLayout renders with dividers 1`] = ` min-width: 1px; } -.c5 { +.c6 { width: 100%; max-width: 100%; margin-left: auto; margin-right: auto; + padding: 0; } -.c10 { +.c11 { margin-left: -16px; margin-right: -16px; display: none; margin-bottom: 16px; } -.c8 { +.c9 { width: 100%; + padding: 0; } -.c9 { +.c10 { -webkit-order: 4; -ms-flex-order: 4; order: 4; @@ -744,7 +792,7 @@ exports[`PageLayout renders with dividers 1`] = ` margin-top: 16px; } -.c6 { +.c7 { -webkit-order: 1; -ms-flex-order: 1; order: 1; @@ -761,7 +809,7 @@ exports[`PageLayout renders with dividers 1`] = ` margin-bottom: 16px; } -.c7 { +.c8 { height: 100%; display: none; margin-left: 16px; @@ -780,14 +828,14 @@ exports[`PageLayout renders with dividers 1`] = ` } @media screen and (min-width:768px) { - .c3 { + .c4 { margin-left: 0 !important; margin-right: 0 !important; } } @media screen and (min-width:1012px) { - .c3 { + .c4 { margin-left: -24px; margin-right: -24px; margin-top: 24px; @@ -795,14 +843,14 @@ exports[`PageLayout renders with dividers 1`] = ` } @media screen and (min-width:768px) { - .c10 { + .c11 { margin-left: 0 !important; margin-right: 0 !important; } } @media screen and (min-width:1012px) { - .c10 { + .c11 { margin-left: -24px; margin-right: -24px; margin-bottom: 24px; @@ -810,25 +858,25 @@ exports[`PageLayout renders with dividers 1`] = ` } @media screen and (min-width:768px) { - .c8 { + .c9 { width: 256px; } } @media screen and (min-width:1012px) { - .c8 { + .c9 { width: 296px; } } @media screen and (min-width:1012px) { - .c9 { + .c10 { margin-top: 24px; } } @media screen and (min-width:768px) { - .c6 { + .c7 { width: auto; margin-right: 16px; margin-top: 0 !important; @@ -843,13 +891,13 @@ exports[`PageLayout renders with dividers 1`] = ` } @media screen and (min-width:1012px) { - .c6 { + .c7 { margin-bottom: 24px; } } @media screen and (min-width:1012px) { - .c7 { + .c8 { margin-left: 24px; } } @@ -864,42 +912,50 @@ exports[`PageLayout renders with dividers 1`] = `
- Header
+ Header +
+
Content
- Footer +
+ Footer +
From 77e7ab057bef5394e757356c58ca7ae0f0bfdb64 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Tue, 2 Aug 2022 11:39:25 -0400 Subject: [PATCH 2/3] Add `InlineAutocomplete` component (#2157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add `useCombobox` hook, extending `@github/combobox-nav` * Add `useSyntheticChange` hook * Add `InlineAutocomplete` component * Refactor and improve comments * Remove extra type * Add story and make it work with `FormControl` * Add to main exports * Add MDX file * Remove unecessary ID on textarea in story * Remove version-lock from new dependencies * Make type of render function more specific * Add unit tests * Simplify `useCombobox` and use `navigate` to focus first item Fixes not having an `aria-activedescendant` initially defined * Fix tests by wrapping `userEvent.type` in `act` * Fix preventing blur when tabbing from loading state * Delete unused imports * Change interfaces out for object types * Add accessible live status message to describe suggestions * Dynamically assign the combobox role to avoid treating the textarea as a combobox when no suggestions are available * Shorten & revise status message * Move to drafts * Move docs to drafts * Fix import in docs * Update combobox-nav dependency * Add option to control whether `Tab` key inserts suggestions * Style the defaulted-to first option differently from the selected option * Update combobox-nav dependency * Update and fix unit tests * Remove unused import (fix lint error) * docs: add drafts metastring * Remove `selectionVariant` from suggestions list * Add `install:docs` script * Add more examples to docs * Add more stories * Fix _another_ bug with the caret-coordinates utility and single-line inputs 🙃 * Move component & hooks to drafts folder * Move stories & tests into drafts * Remove non-null assertions in tests * Move `textarea-caret` type declaration to `@types` * Add props table * Fix TS issue * Create cuddly-bags-sort.md Co-authored-by: Siddharth Kshetrapal --- .changeset/cuddly-bags-sort.md | 5 + @types/@koddsson/index.d.ts | 0 @types/@koddsson/textarea-caret/index.d.ts | 11 + docs/content/drafts/InlineAutocomplete.mdx | 162 ++++++ docs/package-lock.json | 105 ++-- .../src/@primer/gatsby-theme-doctocat/nav.yml | 2 + jest.config.js | 3 +- package-lock.json | 22 + package.json | 3 + src/FormControl/FormControl.tsx | 12 +- .../InlineAutocomplete.stories.tsx | 174 +++++++ .../InlineAutocomplete.test.tsx | 470 ++++++++++++++++++ .../InlineAutocomplete/InlineAutocomplete.tsx | 220 ++++++++ .../_AutocompleteSuggestions.tsx | 119 +++++ src/drafts/InlineAutocomplete/index.ts | 6 + src/drafts/InlineAutocomplete/types.ts | 56 +++ src/drafts/InlineAutocomplete/utils.ts | 160 ++++++ src/drafts/hooks/useCombobox.ts | 153 ++++++ src/drafts/hooks/useSyntheticChange.ts | 149 ++++++ src/drafts/index.ts | 9 + 20 files changed, 1769 insertions(+), 72 deletions(-) create mode 100644 .changeset/cuddly-bags-sort.md create mode 100644 @types/@koddsson/index.d.ts create mode 100644 @types/@koddsson/textarea-caret/index.d.ts create mode 100644 docs/content/drafts/InlineAutocomplete.mdx create mode 100644 src/drafts/InlineAutocomplete/InlineAutocomplete.stories.tsx create mode 100644 src/drafts/InlineAutocomplete/InlineAutocomplete.test.tsx create mode 100644 src/drafts/InlineAutocomplete/InlineAutocomplete.tsx create mode 100644 src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx create mode 100644 src/drafts/InlineAutocomplete/index.ts create mode 100644 src/drafts/InlineAutocomplete/types.ts create mode 100644 src/drafts/InlineAutocomplete/utils.ts create mode 100644 src/drafts/hooks/useCombobox.ts create mode 100644 src/drafts/hooks/useSyntheticChange.ts diff --git a/.changeset/cuddly-bags-sort.md b/.changeset/cuddly-bags-sort.md new file mode 100644 index 00000000000..d9ae0c3c250 --- /dev/null +++ b/.changeset/cuddly-bags-sort.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +Add `InlineAutocomplete` component, `useCombobox` hook, and `useSyntheticChange` hook to drafts diff --git a/@types/@koddsson/index.d.ts b/@types/@koddsson/index.d.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/@types/@koddsson/textarea-caret/index.d.ts b/@types/@koddsson/textarea-caret/index.d.ts new file mode 100644 index 00000000000..c2505997701 --- /dev/null +++ b/@types/@koddsson/textarea-caret/index.d.ts @@ -0,0 +1,11 @@ +declare module '@koddsson/textarea-caret' { + export interface CaretCoordinates { + top: number + left: number + height: number + } + export default function getCaretCoordinates( + input: HTMLTextAreaElement | HTMLInputElement, + index: number + ): CaretCoordinates +} diff --git a/docs/content/drafts/InlineAutocomplete.mdx b/docs/content/drafts/InlineAutocomplete.mdx new file mode 100644 index 00000000000..32fa20c9dcf --- /dev/null +++ b/docs/content/drafts/InlineAutocomplete.mdx @@ -0,0 +1,162 @@ +--- +title: InlineAutocomplete +componentId: inline_autocomplete +status: Draft +description: Provides inline auto completion suggestions for an input or textarea. +source: https://github.com/primer/react/tree/main/src/InlineAutocomplete +storybook: '/react/storybook?path=/story/forms-inlineautocomplete--default' +--- + +```js +import {InlineAutocomplete} from '@primer/react/drafts' +``` + +The `InlineAutocomplete` component extends an `Input` or `Textarea` component to provide inline suggestions, similar to those provided by a code editor. + +## Examples + + + +Input components **must always** be accompanied by a corresponding label to improve support for assistive +technologies. Examples below are provided for conciseness and may not reflect accessibility best practices. + +`InlineAutocomplete` can be used with the [`FormControl`](/FormControl) component to render a corresponding label. + + + +### Multi-line input + +Try typing a `#` symbol to see suggestions. Use `Enter` or click to apply a suggestion. + +```javascript live noinline drafts +const options = ['javascript', 'typescript', 'css', 'html', 'webassembly'] + +const SimpleExample = () => { + const [suggestions, setSuggestions] = React.useState([]) + + return ( + setSuggestions(options.filter(tag => tag.includes(query)))} + onHideSuggestions={() => setSuggestions([])} + > +