@charcoal-ui/react-sandbox 1.0.0-alpha.1

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 (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +18 -0
  3. package/package.json +72 -0
  4. package/src/_lib/compat.ts +15 -0
  5. package/src/components/Carousel/index.story.tsx +86 -0
  6. package/src/components/Carousel/index.tsx +382 -0
  7. package/src/components/CarouselButton/index.story.tsx +44 -0
  8. package/src/components/CarouselButton/index.tsx +162 -0
  9. package/src/components/Filter/index.story.tsx +80 -0
  10. package/src/components/Filter/index.tsx +182 -0
  11. package/src/components/HintText/index.story.tsx +19 -0
  12. package/src/components/HintText/index.tsx +95 -0
  13. package/src/components/Layout/index.story.tsx +121 -0
  14. package/src/components/Layout/index.tsx +363 -0
  15. package/src/components/LeftMenu/index.tsx +68 -0
  16. package/src/components/MenuListItem/index.story.tsx +143 -0
  17. package/src/components/MenuListItem/index.tsx +226 -0
  18. package/src/components/Pager/index.story.tsx +102 -0
  19. package/src/components/Pager/index.tsx +255 -0
  20. package/src/components/Spinner/index.story.tsx +47 -0
  21. package/src/components/Spinner/index.tsx +86 -0
  22. package/src/components/SwitchCheckbox/index.story.tsx +32 -0
  23. package/src/components/SwitchCheckbox/index.tsx +147 -0
  24. package/src/components/TextEllipsis/helper.ts +57 -0
  25. package/src/components/TextEllipsis/index.story.tsx +41 -0
  26. package/src/components/TextEllipsis/index.tsx +35 -0
  27. package/src/components/WithIcon/index.story.tsx +145 -0
  28. package/src/components/WithIcon/index.tsx +158 -0
  29. package/src/components/icons/Base.tsx +75 -0
  30. package/src/components/icons/DotsIcon.tsx +33 -0
  31. package/src/components/icons/InfoIcon.tsx +30 -0
  32. package/src/components/icons/NextIcon.tsx +47 -0
  33. package/src/components/icons/WedgeIcon.tsx +57 -0
  34. package/src/foundation/contants.ts +6 -0
  35. package/src/foundation/hooks.ts +195 -0
  36. package/src/foundation/support.ts +29 -0
  37. package/src/foundation/utils.ts +31 -0
  38. package/src/index.ts +45 -0
  39. package/src/misc/storybook-helper.ts +17 -0
  40. package/src/styled.ts +3 -0
  41. package/src/type.d.ts +12 -0
@@ -0,0 +1,382 @@
1
+ import React, {
2
+ useEffect,
3
+ useState,
4
+ useCallback,
5
+ useRef,
6
+ useLayoutEffect,
7
+ } from 'react'
8
+ import { animated, useSpring } from 'react-spring'
9
+ import styled, { css } from 'styled-components'
10
+ import { useDebounceAnimationState } from '../../foundation/hooks'
11
+ import { passiveEvents, isEdge } from '../../foundation/support'
12
+ import CarouselButton, { Direction } from '../CarouselButton'
13
+
14
+ export const GRADIENT_WIDTH = 72
15
+ /**
16
+ * カルーセル系のスクロール量の定数
17
+ *
18
+ * @example
19
+ * const scrollAmount = containerElm.clientWidth * SCROLL_AMOUNT_COEF
20
+ */
21
+ export const SCROLL_AMOUNT_COEF = 0.75
22
+
23
+ interface ScrollProps {
24
+ align?: 'center' | 'left' | 'right'
25
+ offset?: number
26
+ }
27
+
28
+ export interface CarouselBaseAppearanceProps {
29
+ buttonOffset?: number
30
+ buttonPadding?: number
31
+ bottomOffset?: number
32
+ defaultScroll?: ScrollProps
33
+ }
34
+
35
+ export type CarouselGradientProps =
36
+ | { hasGradient?: false }
37
+ | {
38
+ hasGradient: true
39
+ fadeInGradient?: boolean
40
+ }
41
+
42
+ type CarouselAppearanceProps = CarouselBaseAppearanceProps &
43
+ CarouselGradientProps
44
+
45
+ type Props = CarouselAppearanceProps & {
46
+ onScroll?: (left: number) => void
47
+ onResize?: (width: number) => void
48
+ children: React.ReactNode
49
+ centerItems?: boolean
50
+ onScrollStateChange?: (canScroll: boolean) => void
51
+ }
52
+
53
+ export interface CarouselHandlerRef {
54
+ resetScroll(): void
55
+ }
56
+
57
+ export default function Carousel({
58
+ buttonOffset = 0,
59
+ buttonPadding = 16,
60
+ bottomOffset = 0,
61
+ defaultScroll: { align = 'left', offset: scrollOffset = 0 } = {},
62
+ onScroll,
63
+ onResize,
64
+ children,
65
+ centerItems,
66
+ onScrollStateChange,
67
+ ...options
68
+ }: Props) {
69
+ // スクロール位置を保存する
70
+ // アニメーション中の場合は、アニメーション終了時のスクロール位置が保存される
71
+ const [scrollLeft, setScrollLeft] = useDebounceAnimationState(0)
72
+ // アニメーション中かどうか
73
+ const animation = useRef(false)
74
+ // スクロール可能な領域を保存する
75
+ const [maxScrollLeft, setMaxScrollLeft] = useState(0)
76
+ // 左右のボタンの表示状態を保存する
77
+ const [leftShow, setLeftShow] = useState(false)
78
+ const [rightShow, setRightShow] = useState(false)
79
+
80
+ // const [props, set, stop] = useSpring(() => ({
81
+ // scroll: 0
82
+ // }))
83
+ const [styles, set] = useSpring(() => ({ scroll: 0 }))
84
+
85
+ const ref = useRef<HTMLDivElement>(null)
86
+ const innerRef = useRef<HTMLUListElement>(null)
87
+
88
+ const handleRight = useCallback(() => {
89
+ if (ref.current === null) {
90
+ return
91
+ }
92
+ const { clientWidth } = ref.current
93
+ // スクロール領域を超えないように、アニメーションを開始
94
+ // アニメーション中にアニメーションが開始されたときに、アニメーション終了予定の位置から再度計算するようにする
95
+ const scroll = Math.min(
96
+ scrollLeft + clientWidth * SCROLL_AMOUNT_COEF,
97
+ maxScrollLeft
98
+ )
99
+ setScrollLeft(scroll, true)
100
+ set({ scroll, from: { scroll: scrollLeft }, reset: !animation.current })
101
+ animation.current = true
102
+ }, [animation, maxScrollLeft, scrollLeft, set, setScrollLeft])
103
+
104
+ const handleLeft = useCallback(() => {
105
+ if (ref.current === null) {
106
+ return
107
+ }
108
+ const { clientWidth } = ref.current
109
+ const scroll = Math.max(scrollLeft - clientWidth * SCROLL_AMOUNT_COEF, 0)
110
+ setScrollLeft(scroll, true)
111
+ set({ scroll, from: { scroll: scrollLeft }, reset: !animation.current })
112
+ animation.current = true
113
+ }, [animation, scrollLeft, set, setScrollLeft])
114
+
115
+ // スクロール可能な場合にボタンを表示する
116
+ // scrollLeftが変化したときに処理する (アニメーション開始時 & 手動スクロール時)
117
+ useEffect(() => {
118
+ const newleftShow = scrollLeft > 0
119
+ const newrightShow = scrollLeft < maxScrollLeft && maxScrollLeft > 0
120
+ if (newleftShow !== leftShow || newrightShow !== rightShow) {
121
+ setLeftShow(newleftShow)
122
+ setRightShow(newrightShow)
123
+ onScrollStateChange?.(newleftShow || newrightShow)
124
+ }
125
+ }, [leftShow, maxScrollLeft, onScrollStateChange, rightShow, scrollLeft])
126
+
127
+ const handleScroll = useCallback(() => {
128
+ if (ref.current === null) {
129
+ return
130
+ }
131
+ // 手動でスクロールが開始されたときにアニメーションを中断
132
+ if (animation.current) {
133
+ styles.scroll.stop()
134
+ animation.current = false
135
+ }
136
+ // スクロール位置を保存 (アニメーションの基準になる)
137
+ const manualScrollLeft = ref.current.scrollLeft
138
+ // 過剰にsetStateが走らないようにDebouceする
139
+ setScrollLeft(manualScrollLeft)
140
+ }, [animation, setScrollLeft, styles])
141
+
142
+ // リサイズが起きたときに、アニメーション用のスクロール領域 & ボタンの表示状態 を再計算する
143
+ const handleResize = useCallback(() => {
144
+ if (ref.current === null) {
145
+ return
146
+ }
147
+ const { clientWidth, scrollWidth } = ref.current
148
+ const newMaxScrollLeft = scrollWidth - clientWidth
149
+ setMaxScrollLeft(newMaxScrollLeft)
150
+ if (onResize) {
151
+ onResize(clientWidth)
152
+ }
153
+ }, [onResize])
154
+
155
+ const resizeObserverRef = useRef(new ResizeObserver(handleResize))
156
+ const resizeObserverInnerRef = useRef(new ResizeObserver(handleResize))
157
+
158
+ useLayoutEffect(() => {
159
+ const elm = ref.current
160
+ const innerElm = innerRef.current
161
+ if (elm === null || innerElm === null) {
162
+ return
163
+ }
164
+
165
+ elm.addEventListener(
166
+ 'wheel',
167
+ handleScroll,
168
+ passiveEvents() && { passive: true }
169
+ )
170
+
171
+ const resizeObserver = resizeObserverRef.current
172
+ resizeObserver.observe(elm)
173
+
174
+ const resizeObserverInner = resizeObserverInnerRef.current
175
+ resizeObserverInner.observe(innerElm)
176
+
177
+ return () => {
178
+ elm.removeEventListener('wheel', handleScroll)
179
+ resizeObserver.disconnect()
180
+ resizeObserverInner.disconnect()
181
+ }
182
+ }, [handleResize, handleScroll])
183
+
184
+ // 初期スクロールを行う
185
+ useLayoutEffect(() => {
186
+ if (align !== 'left' || scrollOffset !== 0) {
187
+ const scroll = ref.current
188
+ if (scroll !== null) {
189
+ const scrollLength = Math.max(
190
+ 0,
191
+ Math.min(
192
+ align === 'left' && scrollOffset > 0
193
+ ? scrollOffset
194
+ : align === 'center'
195
+ ? maxScrollLeft / 2 + scrollOffset
196
+ : align === 'right' && scrollOffset <= maxScrollLeft
197
+ ? maxScrollLeft - scrollOffset / 2
198
+ : 0,
199
+ maxScrollLeft
200
+ )
201
+ )
202
+ scroll.scrollLeft = scrollLength
203
+ setScrollLeft(scrollLength, true)
204
+ }
205
+ }
206
+ // eslint-disable-next-line react-hooks/exhaustive-deps
207
+ }, [ref.current])
208
+
209
+ const handleScrollMove = useCallback(() => {
210
+ if (ref.current === null) {
211
+ return
212
+ }
213
+ if (onScroll) {
214
+ onScroll(ref.current.scrollLeft)
215
+ }
216
+ }, [onScroll])
217
+
218
+ // NOTE: Edgeではmaskを使うと要素のレンダリングがバグる(場合によっては画像が表示されない)のでグラデーションを無効にする
219
+ if (!isEdge && options.hasGradient === true) {
220
+ const fadeInGradient = options.fadeInGradient ?? false
221
+ const overflowGradient = !fadeInGradient
222
+ return (
223
+ <Container>
224
+ <GradientContainer fadeInGradient={fadeInGradient}>
225
+ <RightGradient>
226
+ <LeftGradient show={overflowGradient || scrollLeft > 0}>
227
+ <ScrollArea
228
+ ref={ref}
229
+ scrollLeft={styles.scroll}
230
+ onScroll={handleScrollMove}
231
+ >
232
+ <CarouselContainer ref={innerRef} centerItems={centerItems}>
233
+ {children}
234
+ </CarouselContainer>
235
+ </ScrollArea>
236
+ </LeftGradient>
237
+ </RightGradient>
238
+ </GradientContainer>
239
+ <ButtonsContainer>
240
+ <CarouselButton
241
+ direction={Direction.Left}
242
+ show={leftShow}
243
+ offset={buttonOffset}
244
+ bottomOffset={bottomOffset}
245
+ padding={buttonPadding}
246
+ gradient={overflowGradient}
247
+ onClick={handleLeft}
248
+ />
249
+ <CarouselButton
250
+ direction={Direction.Right}
251
+ show={rightShow}
252
+ offset={buttonOffset}
253
+ bottomOffset={bottomOffset}
254
+ padding={buttonPadding}
255
+ gradient
256
+ onClick={handleRight}
257
+ />
258
+ </ButtonsContainer>
259
+ </Container>
260
+ )
261
+ }
262
+
263
+ return (
264
+ <Container>
265
+ <ScrollArea
266
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
267
+ // @ts-expect-error
268
+ ref={ref}
269
+ scrollLeft={styles.scroll}
270
+ onScroll={handleScrollMove}
271
+ >
272
+ <CarouselContainer ref={innerRef} centerItems={centerItems}>
273
+ {children}
274
+ </CarouselContainer>
275
+ </ScrollArea>
276
+ <ButtonsContainer>
277
+ <CarouselButton
278
+ direction={Direction.Left}
279
+ show={leftShow}
280
+ offset={buttonOffset}
281
+ bottomOffset={bottomOffset}
282
+ padding={buttonPadding}
283
+ onClick={handleLeft}
284
+ />
285
+ <CarouselButton
286
+ direction={Direction.Right}
287
+ show={rightShow}
288
+ offset={buttonOffset}
289
+ bottomOffset={bottomOffset}
290
+ padding={buttonPadding}
291
+ onClick={handleRight}
292
+ />
293
+ </ButtonsContainer>
294
+ </Container>
295
+ )
296
+ }
297
+
298
+ const CarouselContainer = styled.ul<{ centerItems?: boolean }>`
299
+ vertical-align: top;
300
+ overflow: hidden;
301
+ list-style: none;
302
+ padding: 0;
303
+
304
+ /* 最小幅を100%にして親要素にぴったりくっつけることで子要素で要素を均等に割り付けるなどを出来るようにしてある */
305
+ min-width: 100%;
306
+ box-sizing: border-box;
307
+
308
+ ${({ centerItems = false }) =>
309
+ centerItems
310
+ ? css`
311
+ display: flex;
312
+ width: max-content;
313
+ margin: 0 auto;
314
+ `
315
+ : css`
316
+ display: inline-flex;
317
+ margin: 0;
318
+ `}
319
+ `
320
+
321
+ const ButtonsContainer = styled.div`
322
+ opacity: 0;
323
+ transition: 0.4s opacity;
324
+ `
325
+ const Container = styled.div`
326
+ &:hover ${ButtonsContainer} {
327
+ opacity: 1;
328
+ }
329
+
330
+ /* CarouselButtonの中にz-index:1があるのでここでコンテキストを切る */
331
+ position: relative;
332
+ z-index: 0;
333
+ `
334
+
335
+ const ScrollArea = styled(animated.div)`
336
+ overflow-x: auto;
337
+ padding: 0;
338
+ margin: 0;
339
+
340
+ &::-webkit-scrollbar {
341
+ display: none;
342
+ }
343
+
344
+ scrollbar-width: none;
345
+ `
346
+
347
+ const GradientContainer = styled.div<{ fadeInGradient: boolean }>`
348
+ /* NOTE: LeftGradientがはみ出るためhidden */
349
+ overflow: hidden;
350
+ ${(p) =>
351
+ !p.fadeInGradient &&
352
+ css`
353
+ margin-left: ${-GRADIENT_WIDTH}px;
354
+ ${CarouselContainer} {
355
+ padding-left: ${GRADIENT_WIDTH}px;
356
+ }
357
+ `}
358
+
359
+ margin-right: ${-GRADIENT_WIDTH}px;
360
+ /* stylelint-disable-next-line no-duplicate-selectors */
361
+ ${CarouselContainer} {
362
+ padding-right: ${GRADIENT_WIDTH}px;
363
+ }
364
+ `
365
+
366
+ const RightGradient = styled.div`
367
+ mask-image: linear-gradient(
368
+ to right,
369
+ #000 calc(100% - ${GRADIENT_WIDTH}px),
370
+ transparent
371
+ );
372
+ `
373
+
374
+ const LeftGradient = styled.div<{ show: boolean }>`
375
+ /* NOTE: mask-position が left → negative px の時、right → abs(negative px) の位置に表示されるため */
376
+ margin-right: ${-GRADIENT_WIDTH}px;
377
+ padding-right: ${GRADIENT_WIDTH}px;
378
+ /* NOTE: mask-position に transition をつけたいが vender prefixes 対策で all につける */
379
+ transition: 0.2s all ease-in;
380
+ mask: linear-gradient(to right, transparent, #000 ${GRADIENT_WIDTH}px)
381
+ ${(p) => (p.show ? 0 : -GRADIENT_WIDTH)}px 0;
382
+ `
@@ -0,0 +1,44 @@
1
+ import { action } from '@storybook/addon-actions'
2
+ import { boolean, number, select, withKnobs } from '@storybook/addon-knobs'
3
+ import React from 'react'
4
+ import CarouselButton, { Direction, ScrollHintButton } from '.'
5
+
6
+ export default {
7
+ title: 'Sandbox/Carousel/CarouselButton',
8
+ decorators: [withKnobs],
9
+ component: CarouselButton,
10
+ }
11
+
12
+ export const _CarouselButton = () => {
13
+ const direction = select(
14
+ 'direction',
15
+ { left: Direction.Left, right: Direction.Right },
16
+ Direction.Left
17
+ )
18
+ const show = boolean('show', true)
19
+ const offset = number('offset', 0)
20
+ const padding = number('padding', 0)
21
+
22
+ return (
23
+ <CarouselButton
24
+ direction={direction}
25
+ show={show}
26
+ offset={offset}
27
+ padding={padding}
28
+ onClick={noop}
29
+ />
30
+ )
31
+ }
32
+
33
+ function noop() {
34
+ // empty
35
+ }
36
+
37
+ export const _ScrollHintButton = () => {
38
+ const direction = select(
39
+ 'direction',
40
+ { left: Direction.Left, right: Direction.Right },
41
+ Direction.Left
42
+ )
43
+ return <ScrollHintButton direction={direction} onClick={action('click')} />
44
+ }
@@ -0,0 +1,162 @@
1
+ import React from 'react'
2
+ import styled, { css } from 'styled-components'
3
+ import { unreachable } from '../../foundation/utils'
4
+ import NextIcon, { WedgeDirection } from '../icons/NextIcon'
5
+ import { applyEffect } from '@charcoal-ui/utils'
6
+
7
+ export enum Direction {
8
+ Right = 'right',
9
+ Left = 'left',
10
+ }
11
+
12
+ interface Props {
13
+ direction: Direction
14
+ show: boolean
15
+ offset?: number
16
+ padding?: number
17
+ bottomOffset?: number
18
+ gradient?: boolean
19
+ onClick(): void
20
+ }
21
+
22
+ export default function CarouselButton({
23
+ direction,
24
+ show,
25
+ offset = 0,
26
+ padding = 0,
27
+ bottomOffset: bottom = 0,
28
+ gradient = false,
29
+ onClick,
30
+ }: Props) {
31
+ const offsetStyle =
32
+ direction === Direction.Left
33
+ ? {
34
+ left: gradient ? offset - 72 : offset,
35
+ paddingLeft: Math.max(padding, 0),
36
+ paddingBottom: bottom,
37
+ }
38
+ : {
39
+ right: gradient ? offset - 72 : offset,
40
+ paddingRight: Math.max(padding, 0),
41
+ paddingBottom: bottom,
42
+ }
43
+ return (
44
+ <Button
45
+ type="button"
46
+ onClick={onClick}
47
+ hide={!show}
48
+ style={offsetStyle}
49
+ css={onlyNonTouchDevice}
50
+ >
51
+ <CarouselButtonIcon>
52
+ <NextIcon
53
+ direction={
54
+ direction === Direction.Right
55
+ ? WedgeDirection.Right
56
+ : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
57
+ direction === Direction.Left
58
+ ? WedgeDirection.Left
59
+ : unreachable()
60
+ }
61
+ />
62
+ </CarouselButtonIcon>
63
+ </Button>
64
+ )
65
+ }
66
+
67
+ export const CAROUSEL_BUTTON_SIZE = 40
68
+
69
+ const CarouselButtonIcon = styled.div`
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ width: ${CAROUSEL_BUTTON_SIZE}px;
74
+ height: ${CAROUSEL_BUTTON_SIZE}px;
75
+ border-radius: 50%;
76
+ background-color: ${({ theme }) => theme.color.surface4};
77
+ transition: 0.4s visibility, 0.4s opacity, 0.2s background-color, 0.2s color;
78
+ color: ${({ theme }) => theme.color.text5};
79
+ `
80
+
81
+ const Button = styled.button<{ hide: boolean }>`
82
+ position: absolute;
83
+ top: 0;
84
+ bottom: 0;
85
+ display: flex;
86
+ align-items: center;
87
+ padding: 0;
88
+ min-width: 40px;
89
+ border: none;
90
+ outline: 0;
91
+ background: transparent;
92
+ cursor: pointer;
93
+ transition: 0.4s visibility, 0.4s opacity;
94
+ /* つらい */
95
+ /* このコンポーネントはCarouselでしか使われてないのでそっちでコンテキストで切る */
96
+ z-index: 1;
97
+
98
+ &:hover ${CarouselButtonIcon} {
99
+ background-color: ${({ theme }) =>
100
+ applyEffect(theme.color.surface4, theme.effect.hover)};
101
+ color: ${({ theme }) => applyEffect(theme.color.text5, theme.effect.hover)};
102
+ }
103
+
104
+ &:active ${CarouselButtonIcon} {
105
+ background-color: ${({ theme }) =>
106
+ applyEffect(theme.color.surface4, theme.effect.press)};
107
+ color: ${({ theme }) => applyEffect(theme.color.text5, theme.effect.press)};
108
+ }
109
+
110
+ ${(p) =>
111
+ p.hide &&
112
+ css`
113
+ visibility: hidden;
114
+ opacity: 0;
115
+ pointer-events: none;
116
+ `}
117
+ `
118
+
119
+ export function ScrollHintButton({
120
+ direction,
121
+ onClick,
122
+ }: {
123
+ direction: Direction
124
+ onClick(): void
125
+ }) {
126
+ return (
127
+ <ScrollHintIcon onClick={onClick}>
128
+ <NextIcon
129
+ direction={
130
+ direction === Direction.Right
131
+ ? WedgeDirection.Right
132
+ : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
133
+ direction === Direction.Left
134
+ ? WedgeDirection.Left
135
+ : unreachable()
136
+ }
137
+ />
138
+ </ScrollHintIcon>
139
+ )
140
+ }
141
+
142
+ const ScrollHintIcon = styled(CarouselButtonIcon)`
143
+ cursor: pointer;
144
+
145
+ &:hover {
146
+ background-color: ${({ theme }) =>
147
+ applyEffect(theme.color.surface4, theme.effect.hover)};
148
+ color: ${({ theme }) => applyEffect(theme.color.text5, theme.effect.hover)};
149
+ }
150
+
151
+ &:active {
152
+ background-color: ${({ theme }) =>
153
+ applyEffect(theme.color.surface4, theme.effect.press)};
154
+ color: ${({ theme }) => applyEffect(theme.color.text5, theme.effect.press)};
155
+ }
156
+ `
157
+
158
+ const onlyNonTouchDevice = css`
159
+ @media (hover: none) and (pointer: coarse) {
160
+ display: none;
161
+ }
162
+ `
@@ -0,0 +1,80 @@
1
+ import { action } from '@storybook/addon-actions'
2
+ import { boolean } from '@storybook/addon-knobs'
3
+ import React from 'react'
4
+ import {
5
+ Link as RouterLink,
6
+ MemoryRouter as Router,
7
+ Route,
8
+ useParams,
9
+ useNavigate,
10
+ } from 'react-router-dom'
11
+ import Filter, { FilterButton, FilterLink } from '.'
12
+ import { ComponentAbstraction } from '@charcoal-ui/react'
13
+
14
+ export default {
15
+ title: 'Sandbox/Filter',
16
+ component: Filter,
17
+ }
18
+
19
+ const makeUrl = (page: number) => `/${page}`
20
+
21
+ export function Default() {
22
+ return (
23
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
24
+ // @ts-expect-error TOOD: adapt to react-router@6 (props should be covariant not bivariant)
25
+ <ComponentAbstraction components={{ Link: RouterLink }}>
26
+ <Router
27
+ initialEntries={Array.from({ length: 5 }).map((_, i) => makeUrl(i + 1))}
28
+ initialIndex={0}
29
+ >
30
+ <Route path="/:page" element={<FilterButtons />} />
31
+ </Router>
32
+ </ComponentAbstraction>
33
+ )
34
+ }
35
+
36
+ function FilterButtons() {
37
+ const navigate = useNavigate()
38
+ const params = useParams()
39
+ const page = params.page !== undefined ? parseInt(params.page, 10) : 0
40
+
41
+ return (
42
+ <Filter>
43
+ <FilterLink to={makeUrl(0)} active={page === 0}>
44
+ 新しい順
45
+ </FilterLink>
46
+ <FilterLink to={makeUrl(1)} active={page === 1}>
47
+ 古い順
48
+ </FilterLink>
49
+ <FilterButton active={page === 2} onClick={() => navigate('2')}>
50
+ 人気順
51
+ </FilterButton>
52
+ <FilterLink to={makeUrl(3)} active={page === 3}>
53
+ これはリンク
54
+ </FilterLink>
55
+ <FilterButton active={page === 4} onClick={() => navigate('4')}>
56
+ これはボタン
57
+ </FilterButton>
58
+ </Filter>
59
+ )
60
+ }
61
+
62
+ export function Button() {
63
+ const active = boolean('Active', true)
64
+ const reactive = boolean('Reactive', false)
65
+ return (
66
+ <FilterButton onClick={action('click')} active={active} reactive={reactive}>
67
+ Label
68
+ </FilterButton>
69
+ )
70
+ }
71
+
72
+ export function Link() {
73
+ const active = boolean('Active', true)
74
+ const reactive = boolean('Reactive', false)
75
+ return (
76
+ <FilterLink to="#" active={active} reactive={reactive}>
77
+ Label
78
+ </FilterLink>
79
+ )
80
+ }