@charcoal-ui/react 2.0.0-alpha.2 → 2.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +16 -0
  2. package/dist/components/Icon/index.d.ts +12 -0
  3. package/dist/components/Icon/index.d.ts.map +1 -0
  4. package/dist/components/Icon/index.story.d.ts +24 -0
  5. package/dist/components/Icon/index.story.d.ts.map +1 -0
  6. package/dist/components/Modal/ModalPlumbing.d.ts +5 -0
  7. package/dist/components/Modal/ModalPlumbing.d.ts.map +1 -0
  8. package/dist/components/Modal/index.d.ts +16 -0
  9. package/dist/components/Modal/index.d.ts.map +1 -0
  10. package/dist/components/Modal/index.story.d.ts +33 -0
  11. package/dist/components/Modal/index.story.d.ts.map +1 -0
  12. package/dist/components/TextField/index.d.ts +6 -3
  13. package/dist/components/TextField/index.d.ts.map +1 -1
  14. package/dist/components/TextField/index.story.d.ts +1 -0
  15. package/dist/components/TextField/index.story.d.ts.map +1 -1
  16. package/dist/core/SSRProvider.d.ts +2 -0
  17. package/dist/core/SSRProvider.d.ts.map +1 -0
  18. package/dist/index.cjs +1060 -1
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.modern.js +944 -45
  23. package/dist/index.modern.js.map +1 -1
  24. package/dist/index.module.js +1039 -1
  25. package/dist/index.module.js.map +1 -1
  26. package/package.json +11 -7
  27. package/src/components/FieldLabel/index.tsx +1 -1
  28. package/src/components/Icon/index.story.tsx +29 -0
  29. package/src/components/Icon/index.tsx +33 -0
  30. package/src/components/Modal/ModalPlumbing.tsx +47 -0
  31. package/src/components/Modal/index.story.tsx +195 -0
  32. package/src/components/Modal/index.tsx +226 -0
  33. package/src/components/TextField/index.story.tsx +31 -16
  34. package/src/components/TextField/index.tsx +51 -30
  35. package/src/components/a11y.test.tsx +11 -0
  36. package/src/core/SSRProvider.tsx +1 -0
  37. package/src/index.ts +4 -0
  38. package/src/styled.ts +1 -1
@@ -0,0 +1,195 @@
1
+ import React from 'react'
2
+ import { Story } from '../../_lib/compat'
3
+ import Modal, { ModalDismissButton, Props } from '.'
4
+ import { OverlayProvider } from '@react-aria/overlays'
5
+ import { useOverlayTriggerState } from 'react-stately'
6
+ import Button from '../Button'
7
+ import {
8
+ ModalAlign,
9
+ ModalBody,
10
+ ModalButtons,
11
+ ModalHeader,
12
+ } from './ModalPlumbing'
13
+ import styled from 'styled-components'
14
+ import { theme } from '../../styled'
15
+ import TextField from '../TextField'
16
+
17
+ export default {
18
+ title: 'Modal',
19
+ component: Modal,
20
+ args: {
21
+ title: 'Title',
22
+ },
23
+ argTypes: {
24
+ size: {
25
+ options: ['S', 'M', 'L'],
26
+ control: {
27
+ type: 'inline-radio',
28
+ },
29
+ },
30
+ bottomSheet: {
31
+ options: ['full', 'true', 'false'],
32
+ mapping: { full: 'full', true: true, false: false },
33
+ control: {
34
+ type: 'inline-radio',
35
+ },
36
+ },
37
+ },
38
+ }
39
+
40
+ const DefaultStory = (args: Props) => {
41
+ const state = useOverlayTriggerState({})
42
+ return (
43
+ // Application must be wrapped in an OverlayProvider so that it can be
44
+ // hidden from screen readers when a modal opens.
45
+ <OverlayProvider>
46
+ <Button onClick={() => state.open()}>Open Modal</Button>
47
+
48
+ <Modal
49
+ isOpen={state.isOpen}
50
+ onClose={() => state.close()}
51
+ isDismissable
52
+ {...args}
53
+ >
54
+ <ModalHeader />
55
+ <ModalBody>
56
+ <ModalVStack>
57
+ <StyledModalText>
58
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod
59
+ placeat tenetur, necessitatibus laudantium cumque exercitationem
60
+ provident. Quaerat iure enim, eveniet dolores earum odio quo
61
+ possimus fugiat aspernatur, numquam, commodi repellat.
62
+ </StyledModalText>
63
+ <ModalAlign>
64
+ <TextField
65
+ showLabel
66
+ label="Name"
67
+ placeholder="Nagisa"
68
+ ></TextField>
69
+ </ModalAlign>
70
+ <ModalAlign>
71
+ <TextField
72
+ showLabel
73
+ label="Country"
74
+ placeholder="Tokyo"
75
+ ></TextField>
76
+ </ModalAlign>
77
+ </ModalVStack>
78
+ <ModalButtons>
79
+ <Button variant="Primary" onClick={() => state.close()} fixed>
80
+ Apply
81
+ </Button>
82
+ <Button onClick={() => state.close()} fixed>
83
+ Cancel
84
+ </Button>
85
+ </ModalButtons>
86
+ </ModalBody>
87
+ </Modal>
88
+ </OverlayProvider>
89
+ )
90
+ }
91
+
92
+ const ModalVStack = styled.div`
93
+ display: grid;
94
+ gap: 24px;
95
+ `
96
+
97
+ const StyledModalText = styled(ModalAlign)`
98
+ ${theme((o) => [o.font.text2, o.typography(14)])}
99
+ `
100
+
101
+ export const Default: Story<Props> = DefaultStory.bind({})
102
+
103
+ const FullBottomSheetStory = (args: Props) => {
104
+ const state = useOverlayTriggerState({})
105
+ return (
106
+ // Application must be wrapped in an OverlayProvider so that it can be
107
+ // hidden from screen readers when a modal opens.
108
+ <OverlayProvider>
109
+ <Button onClick={() => state.open()}>Open Modal</Button>
110
+
111
+ <Modal
112
+ isOpen={state.isOpen}
113
+ onClose={() => state.close()}
114
+ isDismissable
115
+ bottomSheet="full"
116
+ {...args}
117
+ >
118
+ <ModalHeader />
119
+ <ModalBody>
120
+ <ModalVStack>
121
+ <StyledModalText>
122
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod
123
+ placeat tenetur, necessitatibus laudantium cumque exercitationem
124
+ provident. Quaerat iure enim, eveniet dolores earum odio quo
125
+ possimus fugiat aspernatur, numquam, commodi repellat.
126
+ </StyledModalText>
127
+ <ModalAlign>
128
+ <TextField
129
+ showLabel
130
+ label="Name"
131
+ placeholder="Nagisa"
132
+ ></TextField>
133
+ </ModalAlign>
134
+ <ModalAlign>
135
+ <TextField
136
+ showLabel
137
+ label="Country"
138
+ placeholder="Tokyo"
139
+ ></TextField>
140
+ </ModalAlign>
141
+ </ModalVStack>
142
+ <ModalButtons>
143
+ <Button variant="Primary" onClick={() => state.close()} fixed>
144
+ Apply
145
+ </Button>
146
+ <Button onClick={() => state.close()} fixed>
147
+ Cancel
148
+ </Button>
149
+ </ModalButtons>
150
+ </ModalBody>
151
+ </Modal>
152
+ </OverlayProvider>
153
+ )
154
+ }
155
+
156
+ export const FullBottomSheet: Story<Props> = FullBottomSheetStory.bind({})
157
+
158
+ const BottomSheetStory = (args: Props) => {
159
+ const state = useOverlayTriggerState({})
160
+ return (
161
+ // Application must be wrapped in an OverlayProvider so that it can be
162
+ // hidden from screen readers when a modal opens.
163
+ <OverlayProvider>
164
+ <Button onClick={() => state.open()}>Open Modal</Button>
165
+
166
+ <Modal
167
+ isOpen={state.isOpen}
168
+ onClose={() => state.close()}
169
+ bottomSheet
170
+ isDismissable
171
+ {...args}
172
+ >
173
+ <ModalHeader />
174
+ <ModalBody>
175
+ <ModalVStack>
176
+ <StyledModalText>
177
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quod
178
+ placeat tenetur, necessitatibus laudantium cumque exercitationem
179
+ provident. Quaerat iure enim, eveniet dolores earum odio quo
180
+ possimus fugiat aspernatur, numquam, commodi repellat.
181
+ </StyledModalText>
182
+ </ModalVStack>
183
+ <ModalButtons>
184
+ <Button variant="Danger" onClick={() => state.close()} fixed>
185
+ 削除する
186
+ </Button>
187
+ <ModalDismissButton>キャンセル</ModalDismissButton>
188
+ </ModalButtons>
189
+ </ModalBody>
190
+ </Modal>
191
+ </OverlayProvider>
192
+ )
193
+ }
194
+
195
+ export const BottomSheet: Story<Props> = BottomSheetStory.bind({})
@@ -0,0 +1,226 @@
1
+ import React, { useContext, useRef } from 'react'
2
+ import {
3
+ OverlayContainer,
4
+ OverlayProps,
5
+ useModal,
6
+ useOverlay,
7
+ usePreventScroll,
8
+ } from '@react-aria/overlays'
9
+ import styled, { css, useTheme } from 'styled-components'
10
+ import { theme } from '../../styled'
11
+ import { FocusScope } from '@react-aria/focus'
12
+ import { useDialog } from '@react-aria/dialog'
13
+ import { AriaDialogProps } from '@react-types/dialog'
14
+ import { columnSystem, COLUMN_UNIT, GUTTER_UNIT } from '@charcoal-ui/foundation'
15
+ import { unreachable } from '../../_lib'
16
+ import { maxWidth } from '@charcoal-ui/utils'
17
+ import { useMedia } from '@charcoal-ui/styled'
18
+ import { animated, useTransition, easings } from 'react-spring'
19
+ import Button, { ButtonProps } from '../Button'
20
+ import IconButton from '../IconButton'
21
+
22
+ export type Props = OverlayProps &
23
+ AriaDialogProps & {
24
+ children: React.ReactNode
25
+ zIndex?: number
26
+ title: string
27
+ size?: 'S' | 'M' | 'L'
28
+ bottomSheet?: boolean | 'full'
29
+
30
+ // NOTICE: デフォルト値を与えてはならない
31
+ // (たとえば document.body をデフォルト値にすると SSR できなくなる)
32
+ portalContainer?: HTMLElement
33
+ }
34
+
35
+ const DEFAULT_Z_INDEX = 10
36
+
37
+ export default function Modal({
38
+ children,
39
+ zIndex = DEFAULT_Z_INDEX,
40
+ portalContainer,
41
+ ...props
42
+ }: Props) {
43
+ const {
44
+ title,
45
+ size = 'M',
46
+ bottomSheet = false,
47
+ isDismissable,
48
+ onClose,
49
+ isOpen = false,
50
+ } = props
51
+
52
+ const ref = useRef<HTMLDivElement>(null)
53
+ const { overlayProps, underlayProps } = useOverlay(props, ref)
54
+
55
+ usePreventScroll()
56
+ const { modalProps } = useModal()
57
+
58
+ const { dialogProps, titleProps } = useDialog(props, ref)
59
+
60
+ const theme = useTheme()
61
+ const isMobile = useMedia(maxWidth(theme.breakpoint.screen1)) ?? false
62
+ const transitionEnabled = isMobile && bottomSheet !== false
63
+ const transition = useTransition(isOpen, {
64
+ from: {
65
+ transform: 'translateY(100%)',
66
+ backgroundColor: 'rgba(0, 0, 0, 0)',
67
+ },
68
+ enter: {
69
+ transform: 'translateY(0%)',
70
+ backgroundColor: 'rgba(0, 0, 0, 0.4)',
71
+ },
72
+ leave: {
73
+ transform: 'translateY(100%)',
74
+ backgroundColor: 'rgba(0, 0, 0, 0)',
75
+ },
76
+ config: transitionEnabled
77
+ ? { duration: 400, easing: easings.easeOutQuart }
78
+ : { duration: 0 },
79
+ })
80
+ const showDismiss = !isMobile || bottomSheet !== true
81
+
82
+ return transition(
83
+ ({ backgroundColor, transform }, item) =>
84
+ item && (
85
+ <OverlayContainer portalContainer={portalContainer}>
86
+ <ModalBackground
87
+ zIndex={zIndex}
88
+ {...underlayProps}
89
+ style={transitionEnabled ? { backgroundColor } : {}}
90
+ >
91
+ <FocusScope contain restoreFocus autoFocus>
92
+ <ModalDialog
93
+ ref={ref}
94
+ {...overlayProps}
95
+ {...modalProps}
96
+ {...dialogProps}
97
+ style={transitionEnabled ? { transform } : {}}
98
+ size={size}
99
+ bottomSheet={bottomSheet}
100
+ >
101
+ <ModalContext.Provider
102
+ value={{ titleProps, title, close: onClose, showDismiss }}
103
+ >
104
+ {children}
105
+ {isDismissable === true && (
106
+ <ModalCrossButton
107
+ size="S"
108
+ icon="24/Close"
109
+ onClick={onClose}
110
+ />
111
+ )}
112
+ </ModalContext.Provider>
113
+ </ModalDialog>
114
+ </FocusScope>
115
+ </ModalBackground>
116
+ </OverlayContainer>
117
+ )
118
+ )
119
+ }
120
+
121
+ const ModalContext = React.createContext<{
122
+ titleProps: React.HTMLAttributes<HTMLElement>
123
+ title: string
124
+ close?: () => void
125
+ showDismiss: boolean
126
+ }>({
127
+ titleProps: {},
128
+ title: '',
129
+ close: undefined,
130
+ showDismiss: true,
131
+ })
132
+
133
+ const ModalBackground = animated(styled.div<{ zIndex: number }>`
134
+ z-index: ${({ zIndex }) => zIndex};
135
+ position: fixed;
136
+ top: 0;
137
+ left: 0;
138
+ width: 100%;
139
+ height: 100%;
140
+
141
+ ${theme((o) => [o.bg.surface4])}
142
+ `)
143
+
144
+ const ModalDialog = animated(styled.div<{
145
+ size: 'S' | 'M' | 'L'
146
+ bottomSheet: boolean | 'full'
147
+ }>`
148
+ position: absolute;
149
+ top: 50%;
150
+ left: 50%;
151
+ transform: translate(-50%, -50%);
152
+ width: ${(p) =>
153
+ p.size === 'S'
154
+ ? columnSystem(3, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
155
+ : p.size === 'M'
156
+ ? columnSystem(4, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
157
+ : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
158
+ p.size === 'L'
159
+ ? columnSystem(6, COLUMN_UNIT, GUTTER_UNIT) + GUTTER_UNIT * 2
160
+ : unreachable(p.size)}px;
161
+
162
+ ${theme((o) => [o.bg.background1, o.borderRadius(24)])}
163
+
164
+ @media ${({ theme }) => maxWidth(theme.breakpoint.screen1)} {
165
+ ${(p) =>
166
+ p.bottomSheet === 'full'
167
+ ? css`
168
+ top: auto;
169
+ bottom: 0;
170
+ left: 0;
171
+ transform: none;
172
+ border-radius: 0;
173
+ width: 100%;
174
+ height: 100%;
175
+ `
176
+ : p.bottomSheet
177
+ ? css`
178
+ top: auto;
179
+ bottom: 0;
180
+ left: 0;
181
+ transform: none;
182
+ border-radius: 0;
183
+ width: 100%;
184
+ `
185
+ : css`
186
+ width: calc(100% - 48px);
187
+ `}
188
+ }
189
+ `)
190
+
191
+ const ModalCrossButton = styled(IconButton)`
192
+ position: absolute;
193
+ top: 8px;
194
+ right: 8px;
195
+
196
+ ${theme((o) => [o.font.text3.hover.press])}
197
+ `
198
+
199
+ export function ModalTitle(props: React.HTMLAttributes<HTMLHeadingElement>) {
200
+ const { titleProps, title } = useContext(ModalContext)
201
+ return (
202
+ <ModalHeading {...titleProps} {...props}>
203
+ {title}
204
+ </ModalHeading>
205
+ )
206
+ }
207
+
208
+ const ModalHeading = styled.h3`
209
+ margin: 0;
210
+ font-weight: inherit;
211
+ font-size: inherit;
212
+ `
213
+
214
+ export function ModalDismissButton({ children, ...props }: ButtonProps) {
215
+ const { close, showDismiss } = useContext(ModalContext)
216
+
217
+ if (!showDismiss) {
218
+ return null
219
+ }
220
+
221
+ return (
222
+ <Button {...props} onClick={close} fixed>
223
+ {children}
224
+ </Button>
225
+ )
226
+ }
@@ -1,6 +1,6 @@
1
1
  import { action } from '@storybook/addon-actions'
2
2
  import React from 'react'
3
- import { css } from 'styled-components'
3
+ import styled from 'styled-components'
4
4
  import { Story } from '../../_lib/compat'
5
5
  import Clickable from '../Clickable'
6
6
  import TextField, {
@@ -9,6 +9,7 @@ import TextField, {
9
9
  TextFieldProps,
10
10
  } from '.'
11
11
  import { px } from '@charcoal-ui/utils'
12
+ import IconButton from '../IconButton'
12
13
 
13
14
  export default {
14
15
  title: 'TextField',
@@ -24,23 +25,20 @@ export default {
24
25
  },
25
26
  }
26
27
 
28
+ const Container = styled.div`
29
+ display: grid;
30
+ gap: ${({ theme }) => px(theme.spacing[24])};
31
+ `
32
+
27
33
  const Template: Story<Partial<TextFieldProps>> = (args) => (
28
- <div
29
- css={css`
30
- display: grid;
31
- gap: ${({ theme }) => px(theme.spacing[24])};
32
- `}
33
- >
34
+ <Container>
34
35
  <TextField
35
36
  label="Label"
36
37
  requiredText="*必須"
37
38
  subLabel={
38
- <Clickable to="#" onClick={action('click')}>
39
- Text Link
40
- </Clickable>
39
+ <Clickable onClick={action('label-click')}>Text Link</Clickable>
41
40
  }
42
41
  placeholder="Single Line"
43
- onChange={action('change')}
44
42
  {...(args as Partial<SingleLineTextFieldProps>)}
45
43
  multiline={false}
46
44
  />
@@ -48,16 +46,13 @@ const Template: Story<Partial<TextFieldProps>> = (args) => (
48
46
  label="Label"
49
47
  requiredText="*必須"
50
48
  subLabel={
51
- <Clickable to="#" onClick={action('click')}>
52
- Text Link
53
- </Clickable>
49
+ <Clickable onClick={action('label-click')}>Text Link</Clickable>
54
50
  }
55
51
  placeholder="Multi Line"
56
- onChange={action('change')}
57
52
  {...(args as Partial<MultiLineTextFieldProps>)}
58
53
  multiline
59
54
  />
60
- </div>
55
+ </Container>
61
56
  )
62
57
 
63
58
  export const Default = Template.bind({})
@@ -91,3 +86,23 @@ export const AutoHeight: Story<Partial<MultiLineTextFieldProps>> = (args) => (
91
86
  AutoHeight.args = {
92
87
  autoHeight: true,
93
88
  }
89
+
90
+ export const PrefixIcon: Story<Partial<SingleLineTextFieldProps>> = (args) => (
91
+ <TextField
92
+ label="Label"
93
+ placeholder="Icon prefix"
94
+ prefix={
95
+ <PrefixIconWrap>
96
+ <pixiv-icon name="16/Search" />
97
+ </PrefixIconWrap>
98
+ }
99
+ suffix={<IconButton variant="Overlay" icon={'16/Remove'} size="XS" />}
100
+ {...args}
101
+ />
102
+ )
103
+
104
+ const PrefixIconWrap = styled.div`
105
+ color: ${({ theme }) => theme.color.text4};
106
+ margin-top: 2px;
107
+ margin-right: 4px;
108
+ `
@@ -1,9 +1,15 @@
1
1
  import { useTextField } from '@react-aria/textfield'
2
2
  import { useVisuallyHidden } from '@react-aria/visually-hidden'
3
- import React, { useCallback, useEffect, useRef, useState } from 'react'
3
+ import React, {
4
+ ReactNode,
5
+ useCallback,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from 'react'
4
10
  import styled, { css } from 'styled-components'
5
11
  import FieldLabel, { FieldLabelProps } from '../FieldLabel'
6
- import createTheme from '@charcoal-ui/styled'
12
+ import { createTheme } from '@charcoal-ui/styled'
7
13
 
8
14
  const theme = createTheme(styled)
9
15
 
@@ -13,6 +19,9 @@ interface TextFieldBaseProps
13
19
  readonly defaultValue?: string
14
20
  readonly value?: string
15
21
  readonly onChange?: (value: string) => void
22
+ readonly onKeyDown?: (event: React.KeyboardEvent<Element>) => void
23
+ readonly onFocus?: (event: React.FocusEvent<Element>) => void
24
+ readonly onBlur?: (event: React.FocusEvent<Element>) => void
16
25
  readonly showCount?: boolean
17
26
  readonly showLabel?: boolean
18
27
  readonly placeholder?: string
@@ -32,8 +41,8 @@ export interface SingleLineTextFieldProps extends TextFieldBaseProps {
32
41
  readonly multiline?: false
33
42
  readonly rows?: never
34
43
  readonly type?: string
35
- readonly prefix?: string
36
- readonly suffix?: string
44
+ readonly prefix?: ReactNode
45
+ readonly suffix?: ReactNode
37
46
  }
38
47
 
39
48
  export interface MultiLineTextFieldProps extends TextFieldBaseProps {
@@ -94,8 +103,8 @@ const SingleLineTextField = React.forwardRef<
94
103
  invalid = false,
95
104
  assistiveText,
96
105
  maxLength,
97
- prefix = '',
98
- suffix = '',
106
+ prefix = null,
107
+ suffix = null,
99
108
  } = props
100
109
 
101
110
  const { visuallyHiddenProps } = useVisuallyHidden()
@@ -184,9 +193,9 @@ const SingleLineTextField = React.forwardRef<
184
193
  />
185
194
  <SuffixContainer ref={suffixRef}>
186
195
  <Affix>{suffix}</Affix>
187
- {showCount && maxLength && (
196
+ {showCount && (
188
197
  <SingleLineCounter>
189
- {count}/{maxLength}
198
+ {maxLength !== undefined ? `${count}/${maxLength}` : count}
190
199
  </SingleLineCounter>
191
200
  )}
192
201
  </SuffixContainer>
@@ -290,16 +299,23 @@ const MultiLineTextField = React.forwardRef<
290
299
  required={required}
291
300
  subLabel={subLabel}
292
301
  {...labelProps}
293
- {...(showLabel ? visuallyHiddenProps : {})}
302
+ {...(!showLabel ? visuallyHiddenProps : {})}
294
303
  />
295
- <StyledTextareaContainer rows={rows}>
304
+ <StyledTextareaContainer
305
+ invalid={invalid}
306
+ rows={showCount ? rows + 1 : rows}
307
+ >
296
308
  <StyledTextarea
297
309
  ref={mergeRefs(textareaRef, forwardRef, ariaRef)}
298
- invalid={invalid}
299
310
  rows={rows}
311
+ noBottomPadding={showCount}
300
312
  {...inputProps}
301
313
  />
302
- {showCount && <MultiLineCounter>{count}</MultiLineCounter>}
314
+ {showCount && (
315
+ <MultiLineCounter>
316
+ {maxLength !== undefined ? `${count}/${maxLength}` : count}
317
+ </MultiLineCounter>
318
+ )}
303
319
  </StyledTextareaContainer>
304
320
  {assistiveText != null && assistiveText.length !== 0 && (
305
321
  <AssistiveText
@@ -370,8 +386,6 @@ const StyledInput = styled.input<{
370
386
  height: calc(100% / 0.875);
371
387
  font-size: calc(14px / 0.875);
372
388
  line-height: calc(22px / 0.875);
373
- padding-top: calc(9px / 0.875);
374
- padding-bottom: calc(9px / 0.875);
375
389
  padding-left: calc((8px + ${(p) => p.extraLeftPadding}px) / 0.875);
376
390
  padding-right: calc((8px + ${(p) => p.extraRightPadding}px) / 0.875);
377
391
  border-radius: calc(4px / 0.875);
@@ -392,21 +406,35 @@ const StyledInput = styled.input<{
392
406
  }
393
407
  `
394
408
 
395
- const StyledTextareaContainer = styled.div<{ rows: number }>`
396
- display: grid;
409
+ const StyledTextareaContainer = styled.div<{ rows: number; invalid: boolean }>`
397
410
  position: relative;
411
+ overflow: hidden;
412
+ padding: 0 8px;
413
+
414
+ ${(p) =>
415
+ theme((o) => [
416
+ o.bg.surface3.hover,
417
+ p.invalid && o.outline.assertive,
418
+ o.font.text2,
419
+ o.borderRadius(4),
420
+ ])}
421
+
422
+ &:focus-within {
423
+ ${(p) =>
424
+ theme((o) => (p.invalid ? o.outline.assertive : o.outline.default))}
425
+ }
398
426
 
399
427
  ${({ rows }) => css`
400
- max-height: calc(22px * ${rows} + 18px);
428
+ height: calc(22px * ${rows} + 18px);
401
429
  `};
402
430
  `
403
431
 
404
- const StyledTextarea = styled.textarea<{ invalid: boolean }>`
432
+ const StyledTextarea = styled.textarea<{ noBottomPadding: boolean }>`
405
433
  border: none;
406
- box-sizing: border-box;
407
434
  outline: none;
408
435
  resize: none;
409
436
  font-family: inherit;
437
+ color: inherit;
410
438
 
411
439
  /* Prevent zooming for iOS Safari */
412
440
  transform-origin: top left;
@@ -414,23 +442,16 @@ const StyledTextarea = styled.textarea<{ invalid: boolean }>`
414
442
  width: calc(100% / 0.875);
415
443
  font-size: calc(14px / 0.875);
416
444
  line-height: calc(22px / 0.875);
417
- padding: calc(9px / 0.875) calc(8px / 0.875);
418
- border-radius: calc(4px / 0.875);
445
+ padding: calc(9px / 0.875) 0 ${(p) => (p.noBottomPadding ? 0 : '')};
419
446
 
420
- ${({ rows }) => css`
421
- height: calc(22px / 0.875 * ${rows} + 18px / 0.875);
447
+ ${({ rows = 1 }) => css`
448
+ height: calc(22px / 0.875 * ${rows});
422
449
  `};
423
450
 
424
451
  /* Display box-shadow for iOS Safari */
425
452
  appearance: none;
426
453
 
427
- ${(p) =>
428
- theme((o) => [
429
- o.bg.surface3.hover,
430
- o.outline.default.focus,
431
- p.invalid && o.outline.assertive,
432
- o.font.text2,
433
- ])}
454
+ background: none;
434
455
 
435
456
  &::placeholder {
436
457
  ${theme((o) => o.font.text3)}
@@ -63,6 +63,17 @@ beforeEach(() => {
63
63
  return null
64
64
  },
65
65
  }))
66
+
67
+ global.matchMedia = jest.fn().mockImplementation(() => ({
68
+ matches: true,
69
+ media: '(max-width: 600px)',
70
+ addEventListener() {
71
+ // Do Nothing
72
+ },
73
+ removeEventListener() {
74
+ // Do Nothing
75
+ },
76
+ }))
66
77
  })
67
78
 
68
79
  describe.each(themes)('using %s theme', (_name, theme) => {
@@ -0,0 +1 @@
1
+ export { SSRProvider } from '@react-aria/ssr'