@charcoal-ui/react 6.1.0-beta.6 → 8.1.1-beta.0
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.
- package/dist/components/DropdownSelector/Popover/index.d.ts.map +1 -1
- package/dist/components/DropdownSelector/Popover/index2.cjs +32 -1
- package/dist/components/DropdownSelector/Popover/index2.cjs.map +1 -1
- package/dist/components/DropdownSelector/Popover/index2.js +32 -1
- package/dist/components/DropdownSelector/Popover/index2.js.map +1 -1
- package/dist/components/Notification/NotificationRegion.cjs +3 -2
- package/dist/components/Notification/NotificationRegion.cjs.map +1 -1
- package/dist/components/Notification/NotificationRegion.d.ts.map +1 -1
- package/dist/components/Notification/NotificationRegion.js +3 -2
- package/dist/components/Notification/NotificationRegion.js.map +1 -1
- package/dist/components/Notification/types.d.ts +5 -0
- package/dist/components/Notification/types.d.ts.map +1 -1
- package/dist/components/TextField/index.d.ts +0 -2
- package/dist/components/TextField/index.d.ts.map +1 -1
- package/dist/components/TextField/index2.cjs.map +1 -1
- package/dist/components/TextField/index2.js.map +1 -1
- package/dist/index.css +587 -592
- package/dist/layered.css +587 -592
- package/dist/layered.css.map +1 -1
- package/package.json +3 -3
- package/src/components/DropdownSelector/Popover/index.tsx +60 -2
- package/src/components/DropdownSelector/index.browser.test.tsx +78 -3
- package/src/components/DropdownSelector/index.story.tsx +37 -0
- package/src/components/Notification/NotificationRegion.browser.test.tsx +120 -0
- package/src/components/Notification/NotificationRegion.tsx +3 -2
- package/src/components/Notification/Snackbar/__snapshots__/index.css.snap +3 -6
- package/src/components/Notification/Snackbar/index.css +5 -6
- package/src/components/Notification/Snackbar/index.story.tsx +0 -1
- package/src/components/Notification/Toast/__snapshots__/index.css.snap +3 -5
- package/src/components/Notification/Toast/index.css +5 -5
- package/src/components/Notification/Toast/index.story.tsx +0 -1
- package/src/components/Notification/types.ts +5 -0
- package/src/components/TextField/index.tsx +0 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* iOS Safari は非インタラクティブな要素へのペン入力で click を合成しないため、
|
|
12
12
|
* click に依存する react-aria の外側判定では overlay を閉じられない。
|
|
13
13
|
*/
|
|
14
|
-
import { fireEvent, render, screen } from '@testing-library/react'
|
|
14
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
15
15
|
import { vi } from 'vitest'
|
|
16
16
|
import DropdownSelector from '.'
|
|
17
17
|
import DropdownMenuItem from './DropdownMenuItem'
|
|
@@ -60,7 +60,7 @@ describe.each([
|
|
|
60
60
|
['inertWorkaround が無効', false],
|
|
61
61
|
['inertWorkaround が有効', true],
|
|
62
62
|
])('%s なとき', (_, inertWorkaround) => {
|
|
63
|
-
it('Apple Pencil で overlay の外側をタップすると閉じる', () => {
|
|
63
|
+
it('Apple Pencil で overlay の外側をタップすると閉じる', async () => {
|
|
64
64
|
renderSelector(inertWorkaround)
|
|
65
65
|
|
|
66
66
|
penTap(screen.getByRole('button'))
|
|
@@ -68,7 +68,9 @@ describe.each([
|
|
|
68
68
|
|
|
69
69
|
penTap(hitTestOutsidePopover())
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(popover()).toBeNull()
|
|
73
|
+
})
|
|
72
74
|
})
|
|
73
75
|
|
|
74
76
|
// 選択肢のタップは onChange 経由で閉じるのが仕様なので、余白部分を叩く
|
|
@@ -85,6 +87,79 @@ describe.each([
|
|
|
85
87
|
})
|
|
86
88
|
})
|
|
87
89
|
|
|
90
|
+
it('inertWorkaround が有効なとき、開いた直後に underlay へ送られる互換 click では閉じない', () => {
|
|
91
|
+
renderSelector(true)
|
|
92
|
+
|
|
93
|
+
penTap(screen.getByRole('button'))
|
|
94
|
+
expect(popover()).not.toBeNull()
|
|
95
|
+
|
|
96
|
+
// Android Chrome はペンの pointerup 後、underlay 上の pointerdown を伴わない
|
|
97
|
+
// mouse / click イベントを新しく追加された underlay に送る。
|
|
98
|
+
const underlay = getUnderlay()
|
|
99
|
+
fireEvent.mouseDown(underlay)
|
|
100
|
+
fireEvent.mouseUp(underlay)
|
|
101
|
+
fireEvent.click(underlay)
|
|
102
|
+
|
|
103
|
+
expect(popover()).not.toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('ペンで外側をタップしたとき、下にあるクリック可能な要素をクリックせずに閉じる', async () => {
|
|
107
|
+
const handleBackgroundClick = vi.fn()
|
|
108
|
+
render(
|
|
109
|
+
<>
|
|
110
|
+
<DropdownSelector
|
|
111
|
+
label="Label"
|
|
112
|
+
value="1"
|
|
113
|
+
onChange={vi.fn()}
|
|
114
|
+
inertWorkaround
|
|
115
|
+
>
|
|
116
|
+
<DropdownMenuItem value="1">Option 1</DropdownMenuItem>
|
|
117
|
+
<DropdownMenuItem value="2">Option 2</DropdownMenuItem>
|
|
118
|
+
</DropdownSelector>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onClick={handleBackgroundClick}
|
|
122
|
+
style={{ position: 'fixed', right: 16, bottom: 16 }}
|
|
123
|
+
>
|
|
124
|
+
Background button
|
|
125
|
+
</button>
|
|
126
|
+
</>,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
penTap(screen.getByRole('button', { name: 'Label' }))
|
|
130
|
+
expect(popover()).not.toBeNull()
|
|
131
|
+
|
|
132
|
+
const backgroundButton = screen.getByRole('button', {
|
|
133
|
+
name: 'Background button',
|
|
134
|
+
})
|
|
135
|
+
const rect = backgroundButton.getBoundingClientRect()
|
|
136
|
+
const point = {
|
|
137
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
138
|
+
y: Math.round(rect.top + rect.height / 2),
|
|
139
|
+
}
|
|
140
|
+
const pointerTarget = document.elementFromPoint(point.x, point.y)
|
|
141
|
+
if (pointerTarget === null) throw new Error('pointer target not found')
|
|
142
|
+
|
|
143
|
+
fireEvent.pointerDown(pointerTarget, {
|
|
144
|
+
pointerType: 'pen',
|
|
145
|
+
button: 0,
|
|
146
|
+
composed: true,
|
|
147
|
+
})
|
|
148
|
+
// Android Chrome はこの touchstart がキャンセルされた場合、後続の互換
|
|
149
|
+
// mouse / click イベントを生成しない。
|
|
150
|
+
expect(fireEvent.touchStart(pointerTarget)).toBe(false)
|
|
151
|
+
fireEvent.pointerUp(pointerTarget, {
|
|
152
|
+
pointerType: 'pen',
|
|
153
|
+
button: 0,
|
|
154
|
+
composed: true,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
await waitFor(() => {
|
|
158
|
+
expect(popover()).toBeNull()
|
|
159
|
+
})
|
|
160
|
+
expect(handleBackgroundClick).not.toHaveBeenCalled()
|
|
161
|
+
})
|
|
162
|
+
|
|
88
163
|
it('underlay は inertWorkaround が無効なとき inert になる', () => {
|
|
89
164
|
renderSelector(false)
|
|
90
165
|
penTap(screen.getByRole('button'))
|
|
@@ -543,3 +543,40 @@ export const WithRef: StoryObj<typeof DropdownSelector> = {
|
|
|
543
543
|
)
|
|
544
544
|
},
|
|
545
545
|
}
|
|
546
|
+
|
|
547
|
+
export const InertWorkaroundPenOutside: StoryObj<typeof DropdownSelector> = {
|
|
548
|
+
render: function Render() {
|
|
549
|
+
const [selected, setSelected] = useState('1')
|
|
550
|
+
const [backgroundClickCount, setBackgroundClickCount] = useState(0)
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 64 }}>
|
|
554
|
+
<div style={{ width: 288 }}>
|
|
555
|
+
<DropdownSelector
|
|
556
|
+
value={selected}
|
|
557
|
+
onChange={setSelected}
|
|
558
|
+
label="DropdownSelector"
|
|
559
|
+
inertWorkaround
|
|
560
|
+
>
|
|
561
|
+
<DropdownMenuItem value="1">Option 1</DropdownMenuItem>
|
|
562
|
+
<DropdownMenuItem value="2">Option 2</DropdownMenuItem>
|
|
563
|
+
<DropdownMenuItem value="3">Option 3</DropdownMenuItem>
|
|
564
|
+
</DropdownSelector>
|
|
565
|
+
</div>
|
|
566
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
567
|
+
<Button
|
|
568
|
+
data-testid="background-button"
|
|
569
|
+
onClick={() => {
|
|
570
|
+
setBackgroundClickCount((count) => count + 1)
|
|
571
|
+
}}
|
|
572
|
+
>
|
|
573
|
+
Background button
|
|
574
|
+
</Button>
|
|
575
|
+
<div data-testid="background-click-count">
|
|
576
|
+
Background click count: {backgroundClickCount}
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
)
|
|
581
|
+
},
|
|
582
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import type { ComponentType } from 'react'
|
|
3
|
+
import { afterEach } from 'vitest'
|
|
4
|
+
import { useSnackbar } from './Snackbar'
|
|
5
|
+
import { useToast } from './Toast'
|
|
6
|
+
|
|
7
|
+
const HEADER_OFFSET = 64
|
|
8
|
+
const OFFSET = 24
|
|
9
|
+
const MINIMUM_OFFSET = 16
|
|
10
|
+
|
|
11
|
+
const notificationOptions = {
|
|
12
|
+
position: 'top',
|
|
13
|
+
headerOffset: HEADER_OFFSET,
|
|
14
|
+
duration: 60_000,
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
type AppProps = {
|
|
18
|
+
offset?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ToastApp({ offset = OFFSET }: AppProps) {
|
|
22
|
+
const [toast, showToast] = useToast({ ...notificationOptions, offset })
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={() => showToast('保存しました', { type: 'success' })}
|
|
29
|
+
>
|
|
30
|
+
通知を表示
|
|
31
|
+
</button>
|
|
32
|
+
<div style={{ height: '200vh' }}>{toast}</div>
|
|
33
|
+
</>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function SnackbarApp({ offset = OFFSET }: AppProps) {
|
|
38
|
+
const [snackbar, showSnackbar] = useSnackbar({
|
|
39
|
+
...notificationOptions,
|
|
40
|
+
offset,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<button type="button" onClick={() => showSnackbar('保存しました')}>
|
|
46
|
+
通知を表示
|
|
47
|
+
</button>
|
|
48
|
+
<div style={{ height: '200vh' }}>{snackbar}</div>
|
|
49
|
+
</>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getRegions(regionSelector: string) {
|
|
54
|
+
const notificationRegion = screen.getByRole('region')
|
|
55
|
+
const itemRegion = notificationRegion.querySelector(regionSelector)
|
|
56
|
+
|
|
57
|
+
if (!(itemRegion instanceof HTMLElement)) {
|
|
58
|
+
throw new Error('Notification item region not found')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { notificationRegion, itemRegion }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function scrollTo(top: number) {
|
|
65
|
+
window.scrollTo(0, top)
|
|
66
|
+
await vi.waitFor(() => expect(window.scrollY).toBe(top))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
afterEach(async () => {
|
|
70
|
+
await scrollTo(0)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe.each<{
|
|
74
|
+
name: string
|
|
75
|
+
App: ComponentType<AppProps>
|
|
76
|
+
regionSelector: string
|
|
77
|
+
}>([
|
|
78
|
+
{
|
|
79
|
+
name: 'Toast',
|
|
80
|
+
App: ToastApp,
|
|
81
|
+
regionSelector: '.charcoal-toast-region',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'Snackbar',
|
|
85
|
+
App: SnackbarApp,
|
|
86
|
+
regionSelector: '.charcoal-snackbar-region',
|
|
87
|
+
},
|
|
88
|
+
])('$name header offset', ({ App, regionSelector }) => {
|
|
89
|
+
it('ヘッダーと一緒にスクロールし、画面上端からoffsetの位置で固定される', async () => {
|
|
90
|
+
render(<App />)
|
|
91
|
+
fireEvent.click(screen.getByRole('button', { name: '通知を表示' }))
|
|
92
|
+
|
|
93
|
+
const { notificationRegion, itemRegion } = getRegions(regionSelector)
|
|
94
|
+
|
|
95
|
+
expect(itemRegion.getBoundingClientRect().top).toBe(HEADER_OFFSET + OFFSET)
|
|
96
|
+
|
|
97
|
+
await scrollTo(20)
|
|
98
|
+
|
|
99
|
+
expect(notificationRegion.getBoundingClientRect().top).toBe(-20)
|
|
100
|
+
expect(itemRegion.getBoundingClientRect().top).toBe(
|
|
101
|
+
HEADER_OFFSET + OFFSET - 20,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
await scrollTo(HEADER_OFFSET)
|
|
105
|
+
|
|
106
|
+
expect(notificationRegion.getBoundingClientRect().top).toBe(-HEADER_OFFSET)
|
|
107
|
+
expect(itemRegion.getBoundingClientRect().top).toBe(OFFSET)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('offsetが16未満の場合は初期表示位置に16pxの余白を確保する', () => {
|
|
111
|
+
render(<App offset={8} />)
|
|
112
|
+
fireEvent.click(screen.getByRole('button', { name: '通知を表示' }))
|
|
113
|
+
|
|
114
|
+
const { itemRegion } = getRegions(regionSelector)
|
|
115
|
+
|
|
116
|
+
expect(itemRegion.getBoundingClientRect().top).toBe(
|
|
117
|
+
HEADER_OFFSET + MINIMUM_OFFSET,
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -48,6 +48,7 @@ export function NotificationRegion<TContent>({
|
|
|
48
48
|
}
|
|
49
49
|
const { regionProps } = useToastRegion({}, timerState, regionRef)
|
|
50
50
|
const classNames = useClassNames('charcoal-notification-region', className)
|
|
51
|
+
const effectiveOffset = Math.max(offset, DEFAULT_OFFSET)
|
|
51
52
|
|
|
52
53
|
if (state.visibleToasts.length === 0) {
|
|
53
54
|
return null
|
|
@@ -62,14 +63,14 @@ export function NotificationRegion<TContent>({
|
|
|
62
63
|
style={
|
|
63
64
|
{
|
|
64
65
|
zIndex,
|
|
65
|
-
[`--charcoal-${name}-offset`]: `${
|
|
66
|
+
[`--charcoal-${name}-offset`]: `${effectiveOffset}px`,
|
|
66
67
|
justifyContent: position === 'top' ? 'flex-start' : 'flex-end',
|
|
67
68
|
} as CSSProperties
|
|
68
69
|
}
|
|
69
70
|
>
|
|
70
71
|
<div
|
|
71
72
|
style={{
|
|
72
|
-
height:
|
|
73
|
+
height: `calc(${headerOffset}px + env(safe-area-inset-top, 0px) + var(--charcoal-${name}-offset))`,
|
|
73
74
|
}}
|
|
74
75
|
/>
|
|
75
76
|
<div data-position={position} className={`charcoal-${name}-region`}>
|
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
.charcoal-
|
|
1
|
+
.charcoal-notification-region {
|
|
2
2
|
--charcoal-snackbar-offset: 16px;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
.charcoal-snackbar-region[data-position='top'] {
|
|
6
6
|
--charcoal-snackbar-enter-offset: calc(-1 * var(--charcoal-space-30));
|
|
7
7
|
position: sticky;
|
|
8
|
-
top: calc(
|
|
9
|
-
env(safe-area-inset-top, 0px) + max(var(--charcoal-snackbar-offset), 16px)
|
|
10
|
-
);
|
|
8
|
+
top: calc(env(safe-area-inset-top, 0px) + var(--charcoal-snackbar-offset));
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
.charcoal-snackbar-region[data-position='bottom'] {
|
|
14
12
|
--charcoal-snackbar-enter-offset: var(--charcoal-space-30);
|
|
15
13
|
position: fixed;
|
|
16
14
|
bottom: calc(
|
|
17
|
-
env(safe-area-inset-bottom, 0px) +
|
|
18
|
-
max(var(--charcoal-snackbar-offset), 16px)
|
|
15
|
+
env(safe-area-inset-bottom, 0px) + var(--charcoal-snackbar-offset)
|
|
19
16
|
);
|
|
20
17
|
}
|
|
21
18
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
.charcoal-
|
|
1
|
+
.charcoal-notification-region {
|
|
2
2
|
--charcoal-snackbar-offset: 16px;
|
|
3
|
+
}
|
|
3
4
|
|
|
5
|
+
.charcoal-snackbar-region {
|
|
4
6
|
&[data-position='top'] {
|
|
5
7
|
--charcoal-snackbar-enter-offset: calc(-1 * var(--charcoal-space-30));
|
|
6
8
|
|
|
7
9
|
position: sticky;
|
|
8
|
-
top: calc(
|
|
9
|
-
env(safe-area-inset-top, 0px) + max(var(--charcoal-snackbar-offset), 16px)
|
|
10
|
-
);
|
|
10
|
+
top: calc(env(safe-area-inset-top, 0px) + var(--charcoal-snackbar-offset));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
&[data-position='bottom'] {
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
position: fixed;
|
|
17
17
|
bottom: calc(
|
|
18
|
-
env(safe-area-inset-bottom, 0px) +
|
|
19
|
-
max(var(--charcoal-snackbar-offset), 16px)
|
|
18
|
+
env(safe-area-inset-bottom, 0px) + var(--charcoal-snackbar-offset)
|
|
20
19
|
);
|
|
21
20
|
}
|
|
22
21
|
}
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
.charcoal-
|
|
1
|
+
.charcoal-notification-region {
|
|
2
2
|
--charcoal-toast-offset: 16px;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
.charcoal-toast-region[data-position='top'] {
|
|
6
6
|
position: sticky;
|
|
7
7
|
--charcoal-toast-enter-offset: calc(-1 * var(--charcoal-space-30));
|
|
8
|
-
top: calc(
|
|
9
|
-
env(safe-area-inset-top, 0px) + max(var(--charcoal-toast-offset), 16px)
|
|
10
|
-
);
|
|
8
|
+
top: calc(env(safe-area-inset-top, 0px) + var(--charcoal-toast-offset));
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
.charcoal-toast-region[data-position='bottom'] {
|
|
14
12
|
position: fixed;
|
|
15
13
|
--charcoal-toast-enter-offset: var(--charcoal-space-30);
|
|
16
14
|
bottom: calc(
|
|
17
|
-
env(safe-area-inset-bottom, 0px) +
|
|
15
|
+
env(safe-area-inset-bottom, 0px) + var(--charcoal-toast-offset)
|
|
18
16
|
);
|
|
19
17
|
}
|
|
20
18
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
.charcoal-
|
|
1
|
+
.charcoal-notification-region {
|
|
2
2
|
--charcoal-toast-offset: 16px;
|
|
3
|
+
}
|
|
3
4
|
|
|
5
|
+
.charcoal-toast-region {
|
|
4
6
|
&[data-position='top'] {
|
|
5
7
|
position: sticky;
|
|
6
8
|
--charcoal-toast-enter-offset: calc(-1 * var(--charcoal-space-30));
|
|
7
9
|
|
|
8
|
-
top: calc(
|
|
9
|
-
env(safe-area-inset-top, 0px) + max(var(--charcoal-toast-offset), 16px)
|
|
10
|
-
);
|
|
10
|
+
top: calc(env(safe-area-inset-top, 0px) + var(--charcoal-toast-offset));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
&[data-position='bottom'] {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
--charcoal-toast-enter-offset: var(--charcoal-space-30);
|
|
16
16
|
|
|
17
17
|
bottom: calc(
|
|
18
|
-
env(safe-area-inset-bottom, 0px) +
|
|
18
|
+
env(safe-area-inset-bottom, 0px) + var(--charcoal-toast-offset)
|
|
19
19
|
);
|
|
20
20
|
}
|
|
21
21
|
}
|