@charcoal-ui/react 5.5.0-beta.1 → 5.5.0-beta.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charcoal-ui/react",
3
- "version": "5.5.0-beta.1",
3
+ "version": "5.5.0-beta.3",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -54,10 +54,10 @@
54
54
  "react-compiler-runtime": "1.0.0",
55
55
  "react-stately": "^3.26.0",
56
56
  "warning": "^4.0.3",
57
- "@charcoal-ui/foundation": "5.5.0-beta.1",
58
- "@charcoal-ui/theme": "5.5.0-beta.1",
59
- "@charcoal-ui/icons": "5.5.0-beta.1",
60
- "@charcoal-ui/utils": "5.5.0-beta.1"
57
+ "@charcoal-ui/foundation": "5.5.0-beta.3",
58
+ "@charcoal-ui/icons": "5.5.0-beta.3",
59
+ "@charcoal-ui/theme": "5.5.0-beta.3",
60
+ "@charcoal-ui/utils": "5.5.0-beta.3"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=17.0.0"
@@ -1,12 +1,14 @@
1
1
  import { useEffect, useState } from 'react'
2
- import { Meta, StoryObj } from '@storybook/react-webpack5'
3
- import Pagination from '.'
2
+ import { Meta, StoryObj } from '@storybook/react-vite'
3
+ import Pagination, { type PaginationProps } from '.'
4
4
 
5
- type PaginationStoryArgs = {
6
- page: number
7
- pageCount: number
8
- pageRangeDisplayed?: number
9
- }
5
+ type PaginationStoryArgs = Pick<
6
+ PaginationProps,
7
+ 'page' | 'pageCount' | 'pageRangeDisplayed' | 'size'
8
+ >
9
+
10
+ type LinkPaginationStoryArgs = PaginationStoryArgs &
11
+ Pick<PaginationProps, 'linkProps'>
10
12
 
11
13
  function PaginationWithState(args: PaginationStoryArgs) {
12
14
  const [page, setPage] = useState(args.page)
@@ -15,6 +17,7 @@ function PaginationWithState(args: PaginationStoryArgs) {
15
17
  page={page}
16
18
  pageCount={args.pageCount}
17
19
  pageRangeDisplayed={args.pageRangeDisplayed}
20
+ size={args.size}
18
21
  onChange={setPage}
19
22
  />
20
23
  )
@@ -25,7 +28,7 @@ function parsePageFromHash(fallback: number): number {
25
28
  return match ? parseInt(match[1], 10) : fallback
26
29
  }
27
30
 
28
- function LinkPaginationWithState(args: PaginationStoryArgs) {
31
+ function LinkPaginationWithState(args: LinkPaginationStoryArgs) {
29
32
  const [page, setPage] = useState(() => parsePageFromHash(args.page))
30
33
 
31
34
  useEffect(() => {
@@ -43,7 +46,9 @@ function LinkPaginationWithState(args: PaginationStoryArgs) {
43
46
  page={page}
44
47
  pageCount={args.pageCount}
45
48
  pageRangeDisplayed={args.pageRangeDisplayed}
49
+ size={args.size}
46
50
  makeUrl={(p) => `#page-${p}`}
51
+ linkProps={args.linkProps}
47
52
  />
48
53
  </div>
49
54
  )
@@ -55,6 +60,7 @@ export default {
55
60
  parameters: {
56
61
  layout: 'centered',
57
62
  },
63
+ render: (args) => <PaginationWithState {...args} />,
58
64
  } satisfies Meta<typeof Pagination>
59
65
 
60
66
  export const Default: StoryObj<typeof Pagination> = {
@@ -62,7 +68,6 @@ export const Default: StoryObj<typeof Pagination> = {
62
68
  page: 5,
63
69
  pageCount: 10,
64
70
  },
65
- render: (args) => <PaginationWithState {...args} />,
66
71
  }
67
72
 
68
73
  export const FirstPage: StoryObj<typeof Pagination> = {
@@ -70,7 +75,6 @@ export const FirstPage: StoryObj<typeof Pagination> = {
70
75
  page: 1,
71
76
  pageCount: 10,
72
77
  },
73
- render: (args) => <PaginationWithState {...args} />,
74
78
  }
75
79
 
76
80
  export const LastPage: StoryObj<typeof Pagination> = {
@@ -78,7 +82,6 @@ export const LastPage: StoryObj<typeof Pagination> = {
78
82
  page: 10,
79
83
  pageCount: 10,
80
84
  },
81
- render: (args) => <PaginationWithState {...args} />,
82
85
  }
83
86
 
84
87
  export const ManyPages: StoryObj<typeof Pagination> = {
@@ -86,7 +89,23 @@ export const ManyPages: StoryObj<typeof Pagination> = {
86
89
  page: 50,
87
90
  pageCount: 103,
88
91
  },
89
- render: (args) => <PaginationWithState {...args} />,
92
+ }
93
+
94
+ export const SizeS: StoryObj<typeof Pagination> = {
95
+ args: {
96
+ page: 5,
97
+ pageCount: 10,
98
+ size: 'S',
99
+ },
100
+ }
101
+
102
+ export const PageRange5: StoryObj<typeof Pagination> = {
103
+ args: {
104
+ page: 5,
105
+ pageCount: 10,
106
+ pageRangeDisplayed: 5,
107
+ size: 'S',
108
+ },
90
109
  }
91
110
 
92
111
  export const LinkPaginationStory: StoryObj<typeof Pagination> = {
@@ -96,3 +115,19 @@ export const LinkPaginationStory: StoryObj<typeof Pagination> = {
96
115
  },
97
116
  render: (args) => <LinkPaginationWithState {...args} />,
98
117
  }
118
+
119
+ export const LinkPaginationWithLinkProps: StoryObj<typeof Pagination> = {
120
+ args: {
121
+ page: 5,
122
+ pageCount: 10,
123
+ linkProps: { scroll: 'marker' },
124
+ },
125
+ render: (args) => (
126
+ <div>
127
+ <p style={{ marginBottom: 8, fontSize: 14, color: '#666' }}>
128
+ linkProps を渡した例(scroll: &apos;marker&apos; は Next.js Link 用)
129
+ </p>
130
+ <LinkPaginationWithState {...args} />
131
+ </div>
132
+ ),
133
+ }
@@ -0,0 +1,38 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ export type Size = 'S' | 'M'
4
+
5
+ export type PageRangeDisplayed = 5 | 7
6
+
7
+ export type LinkComponentProps = {
8
+ href: string
9
+ className?: string
10
+ children?: React.ReactNode
11
+ }
12
+
13
+ export type PaginationContextValue<T extends React.ElementType = 'a'> = {
14
+ page: number
15
+ pageCount: number
16
+ size: Size
17
+ isLinkMode: boolean
18
+ makeUrl?: (page: number) => string
19
+ LinkComponent: T
20
+ makeClickHandler: (value: number) => () => void
21
+ linkProps?: Omit<React.ComponentPropsWithoutRef<T>, 'href' | 'children'>
22
+ }
23
+
24
+ export const PaginationContext = createContext<PaginationContextValue<
25
+ React.ElementType<LinkComponentProps>
26
+ > | null>(null)
27
+
28
+ export function usePaginationContext<
29
+ T extends React.ElementType = 'a',
30
+ >(): PaginationContextValue<T> {
31
+ const context = useContext(PaginationContext)
32
+ if (context == null) {
33
+ throw new Error(
34
+ 'Pagination components must be used within a Pagination component',
35
+ )
36
+ }
37
+ return context as PaginationContextValue<T>
38
+ }
@@ -1,10 +1,11 @@
1
1
  import { useDebugValue } from 'react'
2
2
  import warning from 'warning'
3
+ import type { PageRangeDisplayed } from './PaginationContext'
3
4
 
4
5
  export function usePaginationWindow(
5
6
  page: number,
6
7
  pageCount: number,
7
- pageRangeDisplayed = 7,
8
+ pageRangeDisplayed: PageRangeDisplayed = 7,
8
9
  ) {
9
10
  'use memo'
10
11
  // ページャーのリンク生成例:
@@ -30,10 +31,9 @@ export function usePaginationWindow(
30
31
  `\`pageCount\` must be integer (${pageCount})`,
31
32
  )
32
33
  warning(
33
- (pageRangeDisplayed | 0) === pageRangeDisplayed,
34
- `\`pageRangeDisplayed\` must be integer (${pageRangeDisplayed})`,
34
+ pageRangeDisplayed === 5 || pageRangeDisplayed === 7,
35
+ `\`pageRangeDisplayed\` must be 5 or 7 (${pageRangeDisplayed})`,
35
36
  )
36
- warning(pageRangeDisplayed > 2, `\`windowSize\` must be greater than 2`)
37
37
  }
38
38
 
39
39
  const visibleFirstPage = 1
@@ -5,35 +5,55 @@
5
5
  }
6
6
 
7
7
  .charcoal-pagination-button {
8
- font-size: 1rem;
9
- line-height: calc(1em + 8px);
10
- text-decoration: none;
11
- border: none;
8
+ cursor: pointer;
9
+ appearance: none;
10
+ padding: 0;
11
+ border-style: none;
12
12
  outline: none;
13
- touch-action: manipulation;
13
+ text-decoration: none;
14
+ font: inherit;
15
+ margin: 0;
14
16
  user-select: none;
15
- transition:
16
- box-shadow 0.2s ease 0s,
17
- color 0.2s ease 0s,
18
- background 0.2s ease 0s,
19
- opacity 0.2s ease 0s;
20
17
 
21
18
  display: flex;
22
- justify-content: center;
23
19
  align-items: center;
24
- box-sizing: content-box;
25
- min-width: 24px;
26
- min-height: 24px;
27
- padding: 8px;
28
- cursor: pointer;
29
- font-weight: bold;
30
- /* HACK: Safari repaint fix */
20
+ justify-content: center;
21
+ font-size: 14px;
22
+ font-weight: 700;
23
+ line-height: 22px;
24
+
25
+ /* HACK:
26
+ * Safari doesn't correctly repaint the elements when they're reordered in response to interaction.
27
+ * This forces it to repaint them. This doesn't work if put on the parents either, has to be here.
28
+ */
31
29
  /* stylelint-disable-next-line property-no-vendor-prefix */
32
30
  -webkit-transform: translateZ(0);
33
31
 
34
- background: transparent;
35
32
  color: var(--charcoal-text3);
36
- border-radius: 48px;
33
+ background-color: var(--charcoal-transparent);
34
+ border-radius: 20px;
35
+ transition:
36
+ 0.2s background-color,
37
+ 0.2s box-shadow;
38
+ }
39
+
40
+ .charcoal-pagination-button:focus {
41
+ outline: none;
42
+ }
43
+
44
+ .charcoal-pagination-button::-moz-focus-inner {
45
+ border-style: none;
46
+ padding: 0;
47
+ }
48
+
49
+ .charcoal-pagination[data-size='S'] .charcoal-pagination-button {
50
+ min-width: 32px;
51
+ min-height: 32px;
52
+ }
53
+
54
+ .charcoal-pagination[data-size='M'] .charcoal-pagination-button {
55
+ min-width: 40px;
56
+ min-height: 40px;
37
57
  }
38
58
 
39
59
  .charcoal-pagination-button[hidden] {
@@ -41,22 +61,60 @@
41
61
  display: block;
42
62
  }
43
63
 
44
- .charcoal-pagination-button:hover {
45
- background: var(--charcoal-surface3);
46
- color: var(--charcoal-text2);
64
+ .charcoal-pagination-button:not(:disabled):not([aria-disabled]):hover,
65
+ .charcoal-pagination-button[aria-disabled='false']:hover {
66
+ color: var(--charcoal-text3);
67
+ background-color: var(--charcoal-surface3);
68
+ }
69
+
70
+ .charcoal-pagination-button:not(:disabled):not([aria-disabled]):active,
71
+ .charcoal-pagination-button[aria-disabled='false']:active {
72
+ color: var(--charcoal-text3);
73
+ background-color: var(--charcoal-surface10);
74
+ }
75
+
76
+ .charcoal-pagination-button:not(:disabled):not([aria-disabled]):focus,
77
+ .charcoal-pagination-button[aria-disabled='false']:focus {
78
+ outline: none;
79
+ box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
80
+ }
81
+
82
+ .charcoal-pagination-button:not(:disabled):not([aria-disabled]):focus-visible,
83
+ .charcoal-pagination-button[aria-disabled='false']:focus-visible {
84
+ box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
85
+ }
86
+
87
+ .charcoal-pagination-button:not(:disabled):not([aria-disabled]):focus:not(
88
+ :focus-visible
89
+ ),
90
+ .charcoal-pagination-button[aria-disabled='false']:focus:not(:focus-visible) {
91
+ box-shadow: none;
47
92
  }
48
93
 
49
94
  .charcoal-pagination-button[aria-current] {
95
+ cursor: default;
50
96
  background-color: var(--charcoal-surface6);
51
97
  color: var(--charcoal-text5);
52
98
  }
53
99
 
54
- .charcoal-pagination-button[aria-current]:hover {
100
+ .charcoal-pagination-button[aria-current]:not(:disabled):not(
101
+ [aria-disabled]
102
+ ):hover,
103
+ .charcoal-pagination-button[aria-current]:not(:disabled):not(
104
+ [aria-disabled]
105
+ ):active {
55
106
  background-color: var(--charcoal-surface6);
56
107
  color: var(--charcoal-text5);
57
108
  }
58
109
 
59
- .charcoal-pagination-spacer {
110
+ .charcoal-pagination-nav-button[hidden] {
111
+ visibility: hidden;
112
+ display: block;
113
+ }
114
+
115
+ .charcoal-pagination-spacer,
116
+ .charcoal-pagination-spacer:hover,
117
+ .charcoal-pagination-spacer:active {
60
118
  cursor: default;
61
119
  color: var(--charcoal-text3);
62
120
  background: none;
@@ -3,17 +3,130 @@ import './index.css'
3
3
  import { usePaginationWindow } from './helper'
4
4
  import { useClassNames } from '../../_lib/useClassNames'
5
5
  import IconButton from '../IconButton'
6
+ import {
7
+ PaginationContext,
8
+ usePaginationContext,
9
+ type LinkComponentProps,
10
+ type PaginationContextValue,
11
+ type PageRangeDisplayed,
12
+ type Size,
13
+ } from './PaginationContext'
14
+
15
+ type NavButtonProps = {
16
+ direction: 'prev' | 'next'
17
+ }
18
+
19
+ function NavButton({ direction }: NavButtonProps) {
20
+ 'use memo'
21
+ const {
22
+ page,
23
+ pageCount,
24
+ size,
25
+ isLinkMode,
26
+ makeUrl,
27
+ LinkComponent,
28
+ makeClickHandler,
29
+ linkProps,
30
+ } = usePaginationContext()
31
+
32
+ const isPrev = direction === 'prev'
33
+ const targetPage = isPrev
34
+ ? Math.max(1, page - 1)
35
+ : Math.min(pageCount, page + 1)
36
+ const disabled = isPrev ? page <= 1 : page >= pageCount
37
+ const navButtonClassName = useClassNames(
38
+ 'charcoal-pagination-nav-button',
39
+ linkProps?.className,
40
+ )
41
+
42
+ return (
43
+ <IconButton
44
+ icon={isPrev ? '24/Prev' : '24/Next'}
45
+ size={size}
46
+ hidden={disabled}
47
+ {...(isLinkMode && makeUrl
48
+ ? {
49
+ component: LinkComponent as 'a',
50
+ href: makeUrl(targetPage),
51
+ 'aria-disabled': disabled,
52
+ ...linkProps,
53
+ className: navButtonClassName,
54
+ }
55
+ : {
56
+ disabled,
57
+ onClick: makeClickHandler(targetPage),
58
+ className: navButtonClassName,
59
+ })}
60
+ />
61
+ )
62
+ }
63
+
64
+ function PageItem({ value }: { value: number | string }) {
65
+ 'use memo'
66
+ const {
67
+ page,
68
+ size,
69
+ isLinkMode,
70
+ makeUrl,
71
+ LinkComponent,
72
+ makeClickHandler,
73
+ linkProps,
74
+ } = usePaginationContext()
75
+ const pageItemClassName = useClassNames(
76
+ 'charcoal-pagination-button',
77
+ linkProps?.className,
78
+ )
79
+
80
+ // 省略記号
81
+ if (value === '...') {
82
+ return (
83
+ <IconButton
84
+ icon="24/Dot"
85
+ size={size}
86
+ disabled
87
+ className="charcoal-pagination-spacer"
88
+ aria-hidden
89
+ />
90
+ )
91
+ }
92
+ // 現在ページ(クリック不可)
93
+ if (value === page) {
94
+ return (
95
+ <span className="charcoal-pagination-button" aria-current="page">
96
+ {value}
97
+ </span>
98
+ )
99
+ }
100
+ if (typeof value !== 'number') return null
101
+ // リンクモード: ページへのリンク
102
+ if (isLinkMode && makeUrl) {
103
+ return (
104
+ <LinkComponent
105
+ href={makeUrl(value)}
106
+ {...linkProps}
107
+ className={pageItemClassName}
108
+ >
109
+ {value}
110
+ </LinkComponent>
111
+ )
112
+ }
113
+ // ボタンモード: クリックでページ遷移
114
+ return (
115
+ <button
116
+ type="button"
117
+ className="charcoal-pagination-button"
118
+ onClick={makeClickHandler(value)}
119
+ >
120
+ {value}
121
+ </button>
122
+ )
123
+ }
6
124
 
7
125
  interface CommonProps {
8
126
  page: number
9
127
  pageCount: number
10
- pageRangeDisplayed?: number
11
- }
12
-
13
- type LinkComponentProps = {
14
- href: string
15
- className?: string
16
- children?: React.ReactNode
128
+ pageRangeDisplayed?: PageRangeDisplayed
129
+ size?: Size
17
130
  }
18
131
 
19
132
  type NavProps = Omit<React.ComponentPropsWithoutRef<'nav'>, 'onChange'>
@@ -32,11 +145,20 @@ type NavProps = Omit<React.ComponentPropsWithoutRef<'nav'>, 'onChange'>
32
145
  * @example
33
146
  * // Link mode with custom component (e.g. Next.js Link)
34
147
  * <Pagination page={1} pageCount={10} makeUrl={(p) => `?page=${p}`} component={Link} />
148
+ *
149
+ * @example
150
+ * // Link mode with linkProps (e.g. Next.js scroll)
151
+ * <Pagination page={1} pageCount={10} makeUrl={(p) => `?page=${p}`} component={Link} linkProps={{ scroll: 'marker' }} />
35
152
  */
36
- export type PaginationProps = CommonProps &
153
+ export type PaginationProps<T extends React.ElementType = 'a'> = CommonProps &
37
154
  NavProps &
38
155
  (
39
- | { onChange(newPage: number): void; makeUrl?: never; component?: never }
156
+ | {
157
+ onChange(newPage: number): void
158
+ makeUrl?: never
159
+ component?: never
160
+ linkProps?: undefined
161
+ }
40
162
  | {
41
163
  makeUrl(page: number): string
42
164
  onChange?: never
@@ -44,110 +166,63 @@ export type PaginationProps = CommonProps &
44
166
  * The component used for link elements. Receives `href`, `className`, and `children`.
45
167
  * @default 'a'
46
168
  */
47
- component?: React.ElementType<LinkComponentProps>
169
+ component?: T
170
+ /**
171
+ * Additional props passed to all link elements (e.g. Next.js Link's scroll, prefetch).
172
+ */
173
+ linkProps?: Omit<React.ComponentPropsWithoutRef<T>, 'href' | 'children'>
48
174
  }
49
175
  )
50
176
 
51
- export default function Pagination({
177
+ export default function Pagination<T extends React.ElementType = 'a'>({
52
178
  page,
53
179
  pageCount,
54
180
  pageRangeDisplayed,
181
+ size = 'M',
55
182
  onChange,
56
183
  makeUrl,
57
- component: LinkComponent = 'a',
184
+ component: LinkComponent = 'a' as T,
185
+ linkProps,
58
186
  className,
59
187
  ...navProps
60
- }: PaginationProps) {
188
+ }: PaginationProps<T>) {
61
189
  'use memo'
62
190
  const window = usePaginationWindow(page, pageCount, pageRangeDisplayed)
63
191
  const isLinkMode = makeUrl !== undefined
64
-
65
- // 'use memo' により React Compiler が自動でメモ化するため useCallback は不要
66
192
  const makeClickHandler = (value: number) => () => onChange?.(value)
67
-
68
193
  const classNames = useClassNames('charcoal-pagination', className)
69
194
 
70
- const NavButton = ({ direction }: { direction: 'prev' | 'next' }) => {
71
- const isPrev = direction === 'prev'
72
- const targetPage = isPrev
73
- ? Math.max(1, page - 1)
74
- : Math.min(pageCount, page + 1)
75
- const disabled = isPrev ? page <= 1 : page >= pageCount
76
-
77
- return (
78
- <IconButton
79
- icon={isPrev ? '24/Prev' : '24/Next'}
80
- size="M"
81
- className="charcoal-pagination-button"
82
- hidden={disabled}
83
- {...(isLinkMode && makeUrl
84
- ? {
85
- component: LinkComponent as 'a',
86
- href: makeUrl(targetPage),
87
- 'aria-disabled': disabled,
88
- }
89
- : {
90
- disabled,
91
- onClick: makeClickHandler(targetPage),
92
- })}
93
- />
94
- )
195
+ const contextValue = {
196
+ page,
197
+ pageCount,
198
+ size,
199
+ isLinkMode,
200
+ makeUrl,
201
+ LinkComponent,
202
+ makeClickHandler,
203
+ linkProps,
95
204
  }
96
205
 
97
- const PageItem = ({ value }: { value: number | string }) => {
98
- // 省略記号
99
- if (value === '...') {
100
- return (
101
- <IconButton
102
- icon="24/Dot"
103
- size="M"
104
- disabled
105
- className="charcoal-pagination-button charcoal-pagination-spacer"
106
- aria-hidden
107
- />
108
- )
109
- }
110
- // 現在ページ(クリック不可)
111
- if (value === page) {
112
- return (
113
- <span className="charcoal-pagination-button" aria-current="page">
114
- {value}
115
- </span>
116
- )
117
- }
118
-
119
- if (typeof value !== 'number') return null
120
-
121
- // リンクモード: ページへのリンク
122
- if (isLinkMode && makeUrl) {
123
- return (
124
- <LinkComponent
125
- href={makeUrl(value)}
126
- className="charcoal-pagination-button"
206
+ return (
207
+ <PaginationContext.Provider
208
+ value={
209
+ contextValue as PaginationContextValue<
210
+ React.ElementType<LinkComponentProps>
127
211
  >
128
- {value}
129
- </LinkComponent>
130
- )
131
- }
132
- // ボタンモード: クリックでページ遷移
133
- return (
134
- <button
135
- type="button"
136
- className="charcoal-pagination-button"
137
- onClick={makeClickHandler(value)}
212
+ }
213
+ >
214
+ <nav
215
+ data-size={size}
216
+ aria-label="Pagination"
217
+ {...navProps}
218
+ className={classNames}
138
219
  >
139
- {value}
140
- </button>
141
- )
142
- }
143
-
144
- return (
145
- <nav {...navProps} className={classNames} aria-label="Pagination">
146
- <NavButton direction="prev" />
147
- {window.map((p) => (
148
- <PageItem key={p} value={p} />
149
- ))}
150
- <NavButton direction="next" />
151
- </nav>
220
+ <NavButton direction="prev" />
221
+ {window.map((p) => (
222
+ <PageItem key={p} value={p} />
223
+ ))}
224
+ <NavButton direction="next" />
225
+ </nav>
226
+ </PaginationContext.Provider>
152
227
  )
153
228
  }
package/src/index.ts CHANGED
@@ -83,3 +83,7 @@ export {
83
83
  default as UnstableTextEllipsis,
84
84
  type TextEllipsisProps,
85
85
  } from './components/TextEllipsis'
86
+ export {
87
+ default as UnstablePagination,
88
+ type PaginationProps,
89
+ } from './components/Pagination'