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'},