Skip to content

Add trailingAction to TextInput #1947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 51 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c9f34f1
adds loading indicator to text inputs
mperrotti Mar 2, 2022
26976c7
removes style debugging div
mperrotti Mar 2, 2022
5157087
updates stories and docs
mperrotti Mar 2, 2022
258ed6c
updates tests
mperrotti Mar 2, 2022
245b568
fixes silly test mistakes
mperrotti Mar 2, 2022
6c1f358
revert default textinput story
mperrotti Mar 2, 2022
81a1022
adds changeset
mperrotti Mar 2, 2022
89b0e44
Merge branch 'main' into mp/text-input-loading-state
mperrotti Mar 2, 2022
e964c11
Merge branch 'main' of github.com:primer/react into mp/text-input-loa…
mperrotti Mar 8, 2022
5388500
Update src/TextInput.tsx
mperrotti Mar 8, 2022
0473333
Merge branch 'mp/text-input-loading-state' of github.com:primer/react…
mperrotti Mar 8, 2022
070612f
updates component API
mperrotti Mar 8, 2022
097e8fb
adds trailing action to text input, tweaks formcontrol
mperrotti Mar 8, 2022
f2f52c7
Merge branch 'main' of github.com:primer/react into mp/text-input-loa…
mperrotti Mar 10, 2022
79fbe76
addresses PR feedback
mperrotti Mar 10, 2022
4a4fcc0
fixes linting error
mperrotti Mar 10, 2022
1bdf6d1
indicate a busy status to assistive technology
mperrotti Mar 10, 2022
5f14db8
Merge branch 'main' of github.com:primer/react into mp/text-input-loa…
mperrotti Mar 11, 2022
3464982
updates snaps
mperrotti Mar 11, 2022
d586a80
renames 'isLoading' prop to 'loading'
mperrotti Mar 15, 2022
ced3de3
Merge branch 'main' of github.com:primer/react into mp/text-input-loa…
mperrotti Mar 15, 2022
8bc639e
Update docs/content/TextInput.mdx
mperrotti Mar 21, 2022
eebc472
Update docs/content/TextInput.mdx
mperrotti Mar 21, 2022
2b18aee
Update docs/content/TextInput.mdx
mperrotti Mar 21, 2022
52d2e1a
Update docs/content/TextInput.mdx
mperrotti Mar 21, 2022
5cfc485
Update docs/content/TextInput.mdx
mperrotti Mar 21, 2022
fc0eeef
Merge branch 'main' of github.com:primer/react into mp/text-input-loa…
mperrotti Mar 21, 2022
8bf281d
updates snapshots
mperrotti Mar 21, 2022
e785ccd
nests examples under an 'Examples' heading
mperrotti Mar 21, 2022
a3dc21d
export TextInput non-pass-through props
mperrotti Mar 21, 2022
fffa764
fix undefined type usage
mperrotti Mar 21, 2022
f9ad318
fixes story types
mperrotti Mar 21, 2022
18d6135
Merge branch 'mp/text-input-loading-state' of github.com:primer/react…
mperrotti Mar 21, 2022
3f6b35e
rm unnecessary width style from formcontrol, fix formcontrol import
mperrotti Mar 21, 2022
8af0c07
Merge branch 'main' of github.com:primer/react into mp/text-input-tra…
mperrotti Mar 21, 2022
16ad284
rm forgotten comment
mperrotti Mar 21, 2022
ac012f1
adds changeset
mperrotti Mar 21, 2022
e94c13c
silences linting error and rms bad imports
mperrotti Mar 21, 2022
2781e4c
Merge branch 'main' of github.com:primer/react into mp/text-input-tra…
mperrotti Mar 24, 2022
46f1daf
addresses PR feedback
mperrotti Mar 24, 2022
0dabb66
rms redundant stories
mperrotti Mar 24, 2022
5781c4d
removes excess right padding when we pass 'trailingAction' and 'loading'
mperrotti Mar 24, 2022
12c5f76
Merge branch 'main' into mp/text-input-trailing-action
mperrotti Mar 24, 2022
b26b765
change iconLabel to aria-label in docs example
mperrotti Mar 25, 2022
f573158
Merge branch 'main' of github.com:primer/react into mp/text-input-tra…
mperrotti Mar 25, 2022
1339d9d
Merge branch 'mp/text-input-trailing-action' of github.com:primer/rea…
mperrotti Mar 25, 2022
6d1e442
empy commit to make CI run
mperrotti Mar 25, 2022
641a276
Merge branch 'main' into mp/text-input-trailing-action
mperrotti Mar 29, 2022
b1d60a2
disable html-addon to prevent Storybook from timing out Chromatic
mperrotti Mar 29, 2022
a16fb1b
Merge branch 'mp/text-input-trailing-action' of github.com:primer/rea…
mperrotti Mar 29, 2022
e73b569
Merge branch 'main' into mp/text-input-trailing-action
mperrotti Mar 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chatty-moose-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Adds the option to render a trailing action inside of the TextInput component
159 changes: 110 additions & 49 deletions docs/content/TextInput.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,42 @@ const WithIconAndLoadingIndicator = () => {
render(<WithIconAndLoadingIndicator />)
```

### With trailing action

```jsx live
<Box display="grid" sx={{gap: 3}}>
<FormControl>
<FormControl.Label>Icon action</FormControl.Label>
<TextInput
trailingAction={
<TextInput.Action
onClick={() => {
alert('clear input')
}}
icon={XIcon}
aria-label="Clear input"
sx={{color: 'fg.subtle'}}
/>
}
/>
</FormControl>
<FormControl>
<FormControl.Label>Text action</FormControl.Label>
<TextInput
trailingAction={
<TextInput.Action
onClick={() => {
alert('clear input')
}}
>
Clear
</TextInput.Action>
}
/>
</FormControl>
</Box>
```

### With error and warning states

```jsx live
Expand Down Expand Up @@ -160,61 +196,63 @@ render(<WithIconAndLoadingIndicator />)

<PropsTable>
<PropsTableRow name="aria-label" type="string" description="Allows input to be accessible." />
<PropsTableRow
name="block"
type="boolean"
defaultValue="false"
description="Creates a full-width input element"
/>
<PropsTableRow name="block" type="boolean" defaultValue="false" description="Creates a full-width input element" />
<PropsTableRow
name="contrast"
type="boolean"
defaultValue="false"
description="Changes background color to a higher contrast color"
/>
<PropsTableRow name='size' type="'small' | 'medium' | 'large'" description="Creates a smaller or larger input than the default." />

<PropsTableRow name="loading" type="boolean" description="Whether to show a loading indicator in the input" />
<PropsTableRow
name="loaderPosition"
type="'auto' | 'leading' | 'trailing'"
description={
<>
<div>Which position to render the loading indicator</div>
<ul>
<li>
'auto' (default): at the end of the input, unless a `leadingVisual` is passed. Then, it will render at the
beginning
</li>
<li>'leading': at the beginning of the input</li>
<li>'trailing': at the end of the input</li>
</ul>
</>
}
/>
<PropsTableRow
name="leadingVisual"
type={<>string | React.ComponentType</>}
description="Visual positioned on the left edge inside the input"
/>
<PropsTableRow name="monospace" type="boolean" defaultValue="false" description="Shows input in monospace font" />
<PropsTableRow
name="trailingVisual"
type={<>string | React.ComponentType</>}
description="Visual positioned on the right edge inside the input"
/>
<PropsTableRow
name="validationStatus"
type="'error' | 'success' | 'warning'"
description="Style the input to match the status"
/>
<PropsTableRow
name="variant"
type="'small' | 'medium' | 'large'"
description="(Use size) Creates a smaller or larger input than the default."
deprecated
/>

<PropsTableRow
name="size"
type="'small' | 'medium' | 'large'"
description="Creates a smaller or larger input than the default."
/>
<PropsTableRow name="loading" type="boolean" description="Whether to show a loading indicator in the input" />
<PropsTableRow
name="loaderPosition"
type="'auto' | 'leading' | 'trailing'"
description={
<>
<div>Which position to render the loading indicator</div>
<ul>
<li>
'auto' (default): at the end of the input, unless a `leadingVisual` is passed. Then, it will render at the
beginning
</li>
<li>'leading': at the beginning of the input</li>
<li>'trailing': at the end of the input</li>
</ul>
</>
}
/>
<PropsTableRow
name="leadingVisual"
type={<>string | React.ComponentType</>}
description="Visual positioned on the left edge inside the input"
/>
<PropsTableRow name="monospace" type="boolean" defaultValue="false" description="Shows input in monospace font" />
<PropsTableRow
name="trailingVisual"
type={<>string | React.ComponentType</>}
description="Visual positioned on the right edge inside the input"
/>
<PropsTableRow
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only thing that was added. The rest of the changes are just indentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a props table for TextInput.Action?

name="trailingAction"
type="React.ReactElement<HTMLProps<HTMLButtonElement>>"
description="A visual that renders inside the input after the typing area"
/>
<PropsTableRow
name="validationStatus"
type="'error' | 'success' | 'warning'"
description="Style the input to match the status"
/>
<PropsTableRow
name="variant"
type="'small' | 'medium' | 'large'"
description="(Use size) Creates a smaller or larger input than the default."
deprecated
/>
<PropsTableRow
name="width"
type={
Expand Down Expand Up @@ -265,6 +303,29 @@ render(<WithIconAndLoadingIndicator />)
/>
</PropsTable>

### TextInput.Action

<PropsTable>
<PropsTableRow
name="aria-label"
type="string"
description="Text that appears in a tooltip. If an icon is passed, this is also used as the label used by assistive technologies."
/>
<PropsTableRow name="icon" type="React.FunctionComponent" description="The icon to render inside the button" />
<PropsTableRow
name="variant"
type="'default' | 'primary' | 'invisible' | 'danger'"
description="Determine's the styles on a button"
/>
<PropsTableBasePropRows
elementType="button"
refType="HTMLButtonElement"
passthroughPropsLink={
<Link href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes">MDN</Link>
}
/>
</PropsTable>

## Status

<ComponentChecklist
Expand Down
4 changes: 2 additions & 2 deletions src/FormControl/FormControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
ref={ref}
display="flex"
flexDirection="column"
width="100%"
sx={{...(isLabelHidden ? {'> *:not(label) + *': {marginTop: 1}} : {'> * + *': {marginTop: 1}}), ...sx}}
>
{slots.Label}
Expand All @@ -184,7 +183,8 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
required,
disabled,
validationStatus,
['aria-describedby']: [validationMessageId, captionId].filter(Boolean).join(' ')
['aria-describedby']: [validationMessageId, captionId].filter(Boolean).join(' '),
...InputComponent.props
})}
{React.Children.toArray(children).filter(
child =>
Expand Down
41 changes: 38 additions & 3 deletions src/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {MouseEventHandler} from 'react'
import React, {MouseEventHandler, useCallback, useState} from 'react'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
import classnames from 'classnames'

Expand All @@ -7,6 +7,7 @@ import {useProvidedRefOrCreate} from './hooks'
import {Merge} from './utils/types'
import TextInputWrapper, {StyledWrapperProps} from './_TextInputWrapper'
import UnstyledTextInput from './_UnstyledTextInput'
import TextInputAction from './_TextInputInnerAction'

export type TextInputNonPassthroughProps = {
/** @deprecated Use `leadingVisual` or `trailingVisual` prop instead */
Expand All @@ -28,6 +29,10 @@ export type TextInputNonPassthroughProps = {
* A visual that renders inside the input after the typing area
*/
trailingVisual?: string | React.ComponentType<{className?: string}>
/**
* A visual that renders inside the input after the typing area
*/
trailingAction?: React.ReactElement<React.HTMLProps<HTMLButtonElement>>
} & Pick<
StyledWrapperProps,
| 'block'
Expand All @@ -52,6 +57,7 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
icon: IconComponent,
leadingVisual: LeadingVisual,
trailingVisual: TrailingVisual,
trailingAction,
block,
className,
contrast,
Expand All @@ -62,6 +68,8 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
validationStatus,
sx: sxProp,
size: sizeProp,
onFocus,
onBlur,
// start deprecated props
width: widthProp,
minWidth: minWidthProp,
Expand All @@ -72,6 +80,7 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
},
ref
) => {
const [isInputFocused, setIsInputFocused] = useState<boolean>(false)
const inputRef = useProvidedRefOrCreate(ref as React.RefObject<HTMLInputElement>)
// this class is necessary to style FilterSearch, plz no touchy!
const wrapperClasses = classnames(className, 'TextInput-wrapper')
Expand All @@ -82,6 +91,20 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
const focusInput: MouseEventHandler = () => {
inputRef.current?.focus()
}
const handleInputFocus = useCallback(
e => {
setIsInputFocused(true)
onFocus && onFocus(e)
},
[onFocus]
)
const handleInputBlur = useCallback(
e => {
setIsInputFocused(false)
onBlur && onBlur(e)
},
[onBlur]
)

return (
<TextInputWrapper
Expand All @@ -99,6 +122,8 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
variant={variantProp}
hasLeadingVisual={Boolean(LeadingVisual || showLeadingLoadingIndicator)}
hasTrailingVisual={Boolean(TrailingVisual || showTrailingLoadingIndicator)}
hasTrailingAction={Boolean(trailingAction)}
isInputFocused={isInputFocused}
onClick={focusInput}
aria-live="polite"
aria-busy={Boolean(loading)}
Expand All @@ -111,14 +136,22 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
>
{typeof LeadingVisual === 'function' ? <LeadingVisual /> : LeadingVisual}
</TextInputInnerVisualSlot>
<UnstyledTextInput ref={inputRef} disabled={disabled} {...inputProps} data-component="input" />
<UnstyledTextInput
ref={inputRef}
disabled={disabled}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
{...inputProps}
data-component="input"
/>
<TextInputInnerVisualSlot
visualPosition="trailing"
showLoadingIndicator={showTrailingLoadingIndicator}
hasLoadingIndicator={typeof loading === 'boolean'}
>
{typeof TrailingVisual === 'function' ? <TrailingVisual /> : TrailingVisual}
</TextInputInnerVisualSlot>
{trailingAction}
</TextInputWrapper>
)
}
Expand All @@ -131,4 +164,6 @@ TextInput.defaultProps = {

TextInput.displayName = 'TextInput'

export default TextInput
export default Object.assign(TextInput, {
Action: TextInputAction
})
1 change: 1 addition & 0 deletions src/_InputLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const InputLabel: React.FC<Props & SxProp> = ({children, disabled, required, vis
display: 'block',
color: disabled ? 'fg.muted' : 'fg.default',
cursor: disabled ? 'default' : 'pointer',
alignSelf: 'flex-start',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not relevant to this PR. I just noticed the label was clickable even if the user's cursor wasn't on the text.

...sx
}}
>
Expand Down
Loading