diff --git a/.changeset/hot-toes-count.md b/.changeset/hot-toes-count.md new file mode 100644 index 00000000000..b485b99ddfb --- /dev/null +++ b/.changeset/hot-toes-count.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Makes it possible to render leading and trailing visuals in TextInputWithTokens just like we do in TextInput diff --git a/docs/content/TextInputWithTokens.mdx b/docs/content/TextInputWithTokens.mdx index 9157be1d0b9..ac91f085b3a 100644 --- a/docs/content/TextInputWithTokens.mdx +++ b/docs/content/TextInputWithTokens.mdx @@ -173,7 +173,7 @@ render(MaxHeightExample) ### With an error validation status ```javascript live noinline -const BasicExample = () => { +const ErrorExample = () => { const [tokens, setTokens] = React.useState([ {text: 'zero', id: 0}, {text: 'one', id: 1}, @@ -198,7 +198,45 @@ const BasicExample = () => { ) } -render(BasicExample) +render(ErrorExample) +``` + +### With leading and trailing visuals + +```javascript live noinline +const LeadingVisualExample = () => { + const [dates, setDates] = React.useState([ + {text: '01 Jan', id: 0}, + {text: '01 Feb', id: 1}, + {text: '01 Mar', id: 2} + ]) + const [tokens, setTokens] = React.useState([ + {text: 'zero', id: 0}, + {text: 'one', id: 1}, + {text: 'two', id: 2} + ]) + const onDateRemove = tokenId => { + setDates(dates.filter(token => token.id !== tokenId)) + } + const onTokenRemove = tokenId => { + setTokens(tokens.filter(token => token.id !== tokenId)) + } + + return ( + <> + + Dates + + + + Tokens + + + + ) +} + +render(LeadingVisualExample) ``` ## Props diff --git a/src/TextInputWithTokens.tsx b/src/TextInputWithTokens.tsx index 8ac994a4c2c..70ff68de655 100644 --- a/src/TextInputWithTokens.tsx +++ b/src/TextInputWithTokens.tsx @@ -66,6 +66,8 @@ const overflowCountFontSizeMap: Record = { function TextInputWithTokensInnerComponent( { icon: IconComponent, + leadingVisual: LeadingVisual, + trailingVisual: TrailingVisual, contrast, className, block, @@ -248,7 +250,8 @@ function TextInputWithTokensInnerComponent + {IconComponent && !LeadingVisual && } + {LeadingVisual && !IconComponent && ( + + {typeof LeadingVisual === 'function' ? : LeadingVisual} + + )} } display="flex" @@ -300,7 +309,6 @@ function TextInputWithTokensInnerComponent - {IconComponent && } ) : null} + {TrailingVisual && ( + + {typeof TrailingVisual === 'function' ? : TrailingVisual} + + )} ) } diff --git a/src/__tests__/TextInputWithTokens.test.tsx b/src/__tests__/TextInputWithTokens.test.tsx index 17d032e3fd6..79cf68a2bad 100644 --- a/src/__tests__/TextInputWithTokens.test.tsx +++ b/src/__tests__/TextInputWithTokens.test.tsx @@ -6,6 +6,7 @@ import 'babel-polyfill' import {TokenSizeKeys, tokenSizes} from '../Token/TokenBase' import {IssueLabelToken} from '../Token' import TextInputWithTokens, {TextInputWithTokensProps} from '../TextInputWithTokens' +import {MarkGithubIcon} from '@primer/octicons-react' expect.extend(toHaveNoViolations) const mockTokens = [ @@ -94,6 +95,20 @@ describe('TextInputWithTokens', () => { ).toMatchSnapshot() }) + it('renders a leadingVisual and trailingVisual', () => { + const onRemoveMock = jest.fn() + expect( + render( + + ) + ).toMatchSnapshot() + }) + it('focuses the previous token when keying ArrowLeft', () => { const onRemoveMock = jest.fn() const {getByLabelText, getByText} = HTMLRender( diff --git a/src/__tests__/__snapshots__/TextInputWithTokens.test.tsx.snap b/src/__tests__/__snapshots__/TextInputWithTokens.test.tsx.snap index c80bec0778d..4c2f31b68d5 100644 --- a/src/__tests__/__snapshots__/TextInputWithTokens.test.tsx.snap +++ b/src/__tests__/__snapshots__/TextInputWithTokens.test.tsx.snap @@ -1,5 +1,731 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`TextInputWithTokens renders a leadingVisual and trailingVisual 1`] = ` +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-left: -0.25rem; + margin-bottom: -0.25rem; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c1 > * { + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + margin-left: 0.25rem; + margin-bottom: 0.25rem; +} + +.c2 { + -webkit-order: 1; + -ms-flex-order: 1; + order: 1; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c3 { + border: 0; + font-size: inherit; + font-family: inherit; + background-color: transparent; + -webkit-appearance: none; + color: inherit; + width: 100%; + height: 100%; +} + +.c3:focus { + outline: 0; +} + +.c0 { + font-size: 14px; + line-height: 20px; + color: #24292f; + vertical-align: middle; + background-color: #ffffff; + border: 1px solid #d0d7de; + border-radius: 6px; + outline: none; + box-shadow: inset 0 1px 0 rgba(208,215,222,0.2); + cursor: text; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + min-height: 32px; + padding-left: 12px; + padding-top: calc(12px / 2); + padding-bottom: calc(12px / 2); + background-repeat: no-repeat; + background-position: right 8px center; + padding-left: 12px; + padding-right: 12px; + padding-left: 12px; + padding-top: calc(12px / 2); + padding-bottom: calc(12px / 2); +} + +.c0::-webkit-input-placeholder { + color: #6e7781; +} + +.c0::-moz-placeholder { + color: #6e7781; +} + +.c0:-ms-input-placeholder { + color: #6e7781; +} + +.c0::placeholder { + color: #6e7781; +} + +.c0:focus-within { + border-color: #0969da; + box-shadow: 0 0 0 3px rgba(9,105,218,0.3); +} + +.c0 > textarea { + padding: 12px; +} + +.c0 >:not(:last-child) { + margin-right: 8px; +} + +.c0 .TextInput-icon { + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + color: #57606a; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.c0 > input, +.c0 > select { + padding-left: 0; + padding-right: 0; +} + +.c6 { + background-color: transparent; + font-family: inherit; + color: currentColor; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + -webkit-text-decoration: none; + text-decoration: none; + padding: 0; + -webkit-transform: translate(1px,-1px); + -ms-transform: translate(1px,-1px); + transform: translate(1px,-1px); + -webkit-align-self: baseline; + -ms-flex-item-align: baseline; + align-self: baseline; + border: 0; + border-radius: 999px; + margin-left: 8px; + height: 32px; + width: 32px; + position: relative; + z-index: 1; +} + +.c6:hover, +.c6:focus { + background-color: rgba(175,184,193,0.2); +} + +.c6:active { + background-color: rgba(234,238,242,0.5); +} + +.c5 { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + background: transparent; + border: none; + color: inherit; + font: inherit; + margin: 0; + overflow: visible; + padding: 0; + width: auto; + -webkit-font-smoothing: inherit; + -moz-osx-font-smoothing: inherit; + -webkit-appearance: none; + line-height: 1; + color: currentColor; + -webkit-text-decoration: none; + text-decoration: none; +} + +.c5:is(a,button,[tabIndex='0']) { + cursor: pointer; +} + +.c5:is(a,button,[tabIndex='0']):after { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; +} + +.c4 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 999px; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + font-weight: 600; + font-family: inherit; + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; + font-size: 14px; + height: 32px; + line-height: 32px; + padding-left: 16px; + padding-right: 16px; + padding-top: 0; + padding-bottom: 0; + background-color: rgba(234,238,242,0.5); + border-color: rgba(27,31,36,0.15); + border-style: solid; + border-width: 1px; + color: #57606a; + max-width: 100%; + padding-right: 0; + position: relative; +} + +.c4:hover { + background-color: rgba(175,184,193,0.2); + color: #24292f; +} + +@media (min-width:768px) { + .c0 { + font-size: 14px; + } +} + + + + +
+
+ +
+ + + zero + + + + + + one + + + + + + two + + + + + + three + + + + + + four + + + + + + five + + + + + + six + + + + + + seven + + + +
+ + +
+`; + exports[`TextInputWithTokens renders a truncated set of tokens 1`] = ` .c1 { display: -webkit-box; diff --git a/src/stories/TextInputWithTokens.stories.tsx b/src/stories/TextInputWithTokens.stories.tsx index 06d9c10d823..88359ee0d97 100644 --- a/src/stories/TextInputWithTokens.stories.tsx +++ b/src/stories/TextInputWithTokens.stories.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useState} from 'react' import {Meta} from '@storybook/react' +import {CheckIcon, NumberIcon} from '@primer/octicons-react' import {BaseStyles, Box, ThemeProvider} from '..' import TextInputWithTokens, {TextInputWithTokensProps} from '../TextInputWithTokens' @@ -98,6 +99,28 @@ export const Default = (args: TextInputWithTokensProps) => { Default.parameters = {controls: {exclude: [excludedControls, 'maxHeight']}} +export const WithLeadingVisual = (args: TextInputWithTokensProps) => { + const [tokens, setTokens] = useState([...mockTokens].slice(0, 3)) + const onTokenRemove: (tokenId: string | number) => void = tokenId => { + setTokens(tokens.filter(token => token.id !== tokenId)) + } + + return +} + +WithLeadingVisual.parameters = {controls: {exclude: [excludedControls, 'maxHeight']}} + +export const WithTrailingVisual = (args: TextInputWithTokensProps) => { + const [tokens, setTokens] = useState([...mockTokens].slice(0, 3)) + const onTokenRemove: (tokenId: string | number) => void = tokenId => { + setTokens(tokens.filter(token => token.id !== tokenId)) + } + + return +} + +WithTrailingVisual.parameters = {controls: {exclude: [excludedControls, 'maxHeight']}} + export const UsingIssueLabelTokens = (args: TextInputWithTokensProps) => { const [tokens, setTokens] = useState([ {text: 'enhancement', id: 1, fillColor: '#a2eeef'},