Skip to content

Commit cf38061

Browse files
authored
Merge branch 'main' into francesca/remove-prop
2 parents 244574c + fcef0ba commit cf38061

10 files changed

+159
-36
lines changed

.github/release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "27.4.0"
2+
".": "27.5.0"
33
}

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [27.5.0](https://github.com/Doist/reactist/compare/v27.4.0...v27.5.0) (2025-03-12)
4+
5+
6+
### 🚀 Features
7+
8+
* Allow positional placement of `endSlot` in `TextField` ([#899](https://github.com/Doist/reactist/issues/899)) ([abb4520](https://github.com/Doist/reactist/commit/abb4520b0688cf462d556d30d49ef164fad20bf4))
9+
310
## [27.4.0](https://github.com/Doist/reactist/compare/v27.3.6...v27.4.0) (2025-02-19)
411

512

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"email": "[email protected]",
77
"url": "http://doist.com"
88
},
9-
"version": "27.4.0",
9+
"version": "27.5.0",
1010
"license": "MIT",
1111
"homepage": "https://github.com/Doist/reactist#readme",
1212
"repository": {
@@ -29,7 +29,7 @@
2929
],
3030
"engines": {
3131
"node": "^16.0.0 || ^18.0.0 || ^20.0.0 || ^21.0.0",
32-
"npm": "^8.3.0 || ^9.0.0 || ^10.0.0"
32+
"npm": "^8.3.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
3333
},
3434
"scripts": {
3535
"postinstall": "patch-package",

src/base-field/base-field.tsx

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,23 @@ export type BaseFieldProps = WithEnhancedClassName &
213213
* @default 'below'
214214
*/
215215
characterCountPosition?: 'below' | 'inline' | 'hidden'
216-
}
216+
} & (
217+
| {
218+
supportsStartAndEndSlots?: false
219+
endSlot?: never
220+
endSlotPosition?: never
221+
}
222+
| {
223+
supportsStartAndEndSlots: true
224+
endSlot?: React.ReactElement | string | number
225+
/**
226+
* This is solely for `bordered` variants of TextField. When set to `bottom` (the default),
227+
* the endSlot will be placed inline with the input field. When set to `fullHeight`, the endSlot
228+
* will be placed to the side of both the input field and the label.
229+
*/
230+
endSlotPosition?: 'bottom' | 'fullHeight'
231+
}
232+
)
217233

218234
type FieldComponentProps<T extends HTMLElement> = Omit<
219235
BaseFieldProps,
@@ -239,6 +255,8 @@ function BaseField({
239255
'aria-describedby': originalAriaDescribedBy,
240256
id: originalId,
241257
characterCountPosition = 'below',
258+
endSlot,
259+
endSlotPosition = 'bottom',
242260
}: BaseFieldProps & BaseFieldVariantProps & WithEnhancedClassName) {
243261
const id = useId(originalId)
244262
const messageId = useId()
@@ -302,36 +320,44 @@ function BaseField({
302320
return (
303321
<Stack space="xsmall" hidden={hidden}>
304322
<Box
323+
display="flex"
324+
flexDirection="row"
305325
className={[
306326
className,
307327
styles.container,
308328
tone === 'error' ? styles.error : null,
309329
variant === 'bordered' ? styles.bordered : null,
310330
]}
311331
maxWidth={maxWidth}
332+
alignItems="center"
312333
>
313-
{label || auxiliaryLabel ? (
314-
<Box
315-
as="span"
316-
display="flex"
317-
justifyContent="spaceBetween"
318-
alignItems="flexEnd"
319-
>
320-
<Text
321-
size={variant === 'bordered' ? 'caption' : 'body'}
322-
as="label"
323-
htmlFor={id}
334+
<Box flexGrow={1}>
335+
{label || auxiliaryLabel ? (
336+
<Box
337+
as="span"
338+
display="flex"
339+
justifyContent="spaceBetween"
340+
alignItems="flexEnd"
324341
>
325-
{label ? <span className={styles.primaryLabel}>{label}</span> : null}
326-
</Text>
327-
{auxiliaryLabel ? (
328-
<Box className={styles.auxiliaryLabel} paddingLeft="small">
329-
{auxiliaryLabel}
330-
</Box>
331-
) : null}
332-
</Box>
333-
) : null}
334-
{children(childrenProps)}
342+
<Text
343+
size={variant === 'bordered' ? 'caption' : 'body'}
344+
as="label"
345+
htmlFor={id}
346+
>
347+
{label ? (
348+
<span className={styles.primaryLabel}>{label}</span>
349+
) : null}
350+
</Text>
351+
{auxiliaryLabel ? (
352+
<Box className={styles.auxiliaryLabel} paddingLeft="small">
353+
{auxiliaryLabel}
354+
</Box>
355+
) : null}
356+
</Box>
357+
) : null}
358+
{children(childrenProps)}
359+
</Box>
360+
{endSlot && endSlotPosition === 'fullHeight' ? endSlot : null}
335361
</Box>
336362

337363
{message || characterCount ? (

src/select-field/select-field.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import { Box } from '../box'
44
import styles from './select-field.module.css'
55

66
interface SelectFieldProps
7-
extends Omit<FieldComponentProps<HTMLSelectElement>, 'maxLength' | 'characterCountPosition'>,
7+
extends Omit<
8+
FieldComponentProps<HTMLSelectElement>,
9+
| 'maxLength'
10+
| 'characterCountPosition'
11+
| 'endSlot'
12+
| 'supportsStartAndEndSlots'
13+
| 'endSlotPosition'
14+
>,
815
BaseFieldVariantProps {}
916

1017
const SelectField = React.forwardRef<HTMLSelectElement, SelectFieldProps>(function SelectField(

src/text-area/text-area.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import styles from './text-area.module.css'
77

88
interface TextAreaProps
99
extends Omit<FieldComponentProps<HTMLTextAreaElement>, 'characterCountPosition'>,
10-
BaseFieldVariantProps {
10+
Omit<BaseFieldVariantProps, 'supportsStartAndEndSlots' | 'endSlot' | 'endSlotPosition'> {
1111
/**
1212
* The number of visible text lines for the text area.
1313
*

src/text-field/text-field.stories.mdx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TextField } from './'
44
import { Box } from '../box'
55
import { Stack } from '../stack'
66
import { Text } from '../text'
7-
import { Button } from '../button'
7+
import { Button, IconButton } from '../button'
88
import { Tooltip } from '../tooltip'
99

1010
import { selectWithNone } from '../utils/storybook-helper'
@@ -130,6 +130,16 @@ export function InteractivePropsStory({
130130
control: { type: 'boolean' },
131131
defaultValue: false,
132132
},
133+
characterCountPosition: {
134+
options: [undefined, 'hidden', 'inline', 'below'],
135+
control: { type: 'inline-radio' },
136+
defaultValue: undefined,
137+
},
138+
endSlotPosition: {
139+
options: [undefined, 'bottom', 'fullHeight'],
140+
control: { type: 'inline-radio' },
141+
defaultValue: undefined,
142+
},
133143
}}
134144
>
135145
{InteractivePropsStory.bind({})}
@@ -256,7 +266,7 @@ export function ClearButtonIcon() {
256266
export function ClearButtonExample({ slot }) {
257267
const [value, setValue] = React.useState('')
258268
const clearButton = (
259-
<Button
269+
<IconButton
260270
variant="quaternary"
261271
icon={<ClearButtonIcon />}
262272
aria-label="Clear search"
@@ -325,6 +335,38 @@ Hence, the description is never needed when the field is not focused anyway.
325335
</Story>
326336
</Canvas>
327337

338+
## Variants
339+
340+
The `variant` prop is used to change the style of the text field. The default variant is `default`. The other variant is `bordered`.
341+
342+
export function WithBorderedExample() {
343+
return (
344+
<TextField label="Company name" placeholder="Text field with a border" variant="bordered" />
345+
)
346+
}
347+
348+
export function WithBorderedAndEndSlotExample({ endSlotPosition = 'fullHeight' } = {}) {
349+
return (
350+
<TextField
351+
label="Company name"
352+
placeholder="Text field with a border and end slot"
353+
variant="bordered"
354+
endSlot={<IconButton variant="primary" icon="😄" aria-label="Say cheese!" />}
355+
endSlotPosition={endSlotPosition}
356+
/>
357+
)
358+
}
359+
360+
<Canvas withToolbar>
361+
<Story name="Bordered variant">
362+
<Stack space="xxlarge" dividers="secondary">
363+
<WithBorderedExample />
364+
<WithBorderedAndEndSlotExample />
365+
<WithBorderedAndEndSlotExample endSlotPosition="bottom" />
366+
</Stack>
367+
</Story>
368+
</Canvas>
369+
328370
## Max length
329371

330372
The `maxLength` prop is used to limit the number of characters that the user can input. When the user tries to input more characters than the maximum allowed, the field won't accept the input.

src/text-field/text-field.test.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react'
22
import { render, screen } from '@testing-library/react'
3-
import { TextField } from './'
3+
import { TextField, TextFieldProps } from './'
44
import userEvent from '@testing-library/user-event'
55
import { axe } from 'jest-axe'
66

@@ -247,6 +247,39 @@ describe('TextField', () => {
247247
})
248248
})
249249

250+
describe('endSlotPosition', () => {
251+
test.each<TextFieldProps['endSlotPosition']>(['bottom', 'fullHeight', undefined])(
252+
'renders the end slot for default variant when endSlotPosition is %s',
253+
(endSlotPosition) => {
254+
render(
255+
<TextField
256+
label="Whatʼs your name?"
257+
maxLength={30}
258+
endSlot="Kwijibo"
259+
endSlotPosition={endSlotPosition}
260+
/>,
261+
)
262+
expect(screen.getByText('Kwijibo')).toBeInTheDocument()
263+
},
264+
)
265+
266+
test.each<TextFieldProps['endSlotPosition']>(['bottom', 'fullHeight', undefined])(
267+
'renders the end slot for bordered variant when endSlotPosition is %s',
268+
(endSlotPosition) => {
269+
render(
270+
<TextField
271+
label="Whatʼs your name?"
272+
maxLength={30}
273+
endSlot="Kwijibo"
274+
endSlotPosition={endSlotPosition}
275+
variant="bordered"
276+
/>,
277+
)
278+
expect(screen.getByText('Kwijibo')).toBeInTheDocument()
279+
},
280+
)
281+
})
282+
250283
describe('character count', () => {
251284
it('renders the character count element when characterCountPosition is "below"', () => {
252285
render(

src/text-field/text-field.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useMergeRefs } from 'use-callback-ref'
88
type TextFieldType = 'email' | 'search' | 'tel' | 'text' | 'url'
99

1010
interface TextFieldProps
11-
extends Omit<FieldComponentProps<HTMLInputElement>, 'type'>,
11+
extends Omit<FieldComponentProps<HTMLInputElement>, 'type' | 'supportsStartAndEndSlots'>,
1212
BaseFieldVariantProps,
1313
Pick<BaseFieldProps, 'characterCountPosition'> {
1414
type?: TextFieldType
@@ -40,6 +40,7 @@ const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(function Te
4040
endSlot,
4141
onChange: originalOnChange,
4242
characterCountPosition = 'below',
43+
endSlotPosition = 'bottom',
4344
...props
4445
},
4546
ref,
@@ -52,6 +53,10 @@ const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(function Te
5253
internalRef.current?.focus()
5354
}
5455

56+
const displayEndSlot =
57+
endSlot &&
58+
(variant === 'default' || (variant === 'bordered' && endSlotPosition === 'bottom'))
59+
5560
return (
5661
<BaseField
5762
variant={variant}
@@ -66,6 +71,9 @@ const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(function Te
6671
hidden={hidden}
6772
aria-describedby={ariaDescribedBy}
6873
characterCountPosition={characterCountPosition}
74+
supportsStartAndEndSlots
75+
endSlot={endSlot}
76+
endSlotPosition={variant === 'bordered' ? endSlotPosition : undefined}
6977
>
7078
{({ onChange, characterCountElement, ...extraProps }) => (
7179
<Box
@@ -100,15 +108,15 @@ const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(function Te
100108
onChange?.(event)
101109
}}
102110
/>
103-
{endSlot || characterCountElement ? (
111+
{displayEndSlot || characterCountElement ? (
104112
<Box
105113
className={styles.slot}
106114
display="flex"
107115
marginRight={variant === 'bordered' ? '-xsmall' : 'xsmall'}
108116
marginLeft={variant === 'bordered' ? 'xsmall' : '-xsmall'}
109117
>
110118
{characterCountElement}
111-
{endSlot}
119+
{displayEndSlot ? endSlot : null}
112120
</Box>
113121
) : null}
114122
</Box>

0 commit comments

Comments
 (0)