@charcoal-ui/react 5.5.0-beta.0 → 5.5.0-beta.2

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.0",
3
+ "version": "5.5.0-beta.2",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -48,16 +48,16 @@
48
48
  "@react-aria/switch": "^3.6.0",
49
49
  "@react-aria/utils": "^3.23.0",
50
50
  "@react-aria/visually-hidden": "^3.8.8",
51
+ "@react-spring/web": "^10",
51
52
  "@react-stately/radio": "^3.10.2",
52
53
  "polished": "^4.1.4",
53
54
  "react-compiler-runtime": "1.0.0",
54
- "react-spring": "^9.0.0",
55
55
  "react-stately": "^3.26.0",
56
56
  "warning": "^4.0.3",
57
- "@charcoal-ui/foundation": "5.5.0-beta.0",
58
- "@charcoal-ui/icons": "5.5.0-beta.0",
59
- "@charcoal-ui/utils": "5.5.0-beta.0",
60
- "@charcoal-ui/theme": "5.5.0-beta.0"
57
+ "@charcoal-ui/foundation": "5.5.0-beta.2",
58
+ "@charcoal-ui/theme": "5.5.0-beta.2",
59
+ "@charcoal-ui/utils": "5.5.0-beta.2",
60
+ "@charcoal-ui/icons": "5.5.0-beta.2"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=17.0.0"
@@ -2,7 +2,7 @@ import { useContext, forwardRef, memo } from 'react'
2
2
  import * as React from 'react'
3
3
  import { Overlay } from '@react-aria/overlays'
4
4
  import type { AriaDialogProps } from '@react-types/dialog'
5
- import { animated, useTransition, easings } from 'react-spring'
5
+ import { animated, useTransition, easings } from '@react-spring/web'
6
6
  import Button, { ButtonProps } from '../Button'
7
7
  import IconButton, { IconButtonProps } from '../IconButton'
8
8
  import { useObjectRef } from '@react-aria/utils'
@@ -1,10 +1,53 @@
1
- import { useState } from 'react'
2
- import { Meta, StoryObj } from '@storybook/react-webpack5'
3
- import Pagination, { LinkPagination } from '.'
1
+ import { useEffect, useState } from 'react'
2
+ import { Meta, StoryObj } from '@storybook/react-vite'
3
+ import Pagination, { type PaginationProps } from '.'
4
4
 
5
- function PaginationWithState(args: React.ComponentProps<typeof Pagination>) {
5
+ type PaginationStoryArgs = Pick<
6
+ PaginationProps,
7
+ 'page' | 'pageCount' | 'pageRangeDisplayed' | 'size'
8
+ >
9
+
10
+ function PaginationWithState(args: PaginationStoryArgs) {
6
11
  const [page, setPage] = useState(args.page)
7
- return <Pagination {...args} page={page} onChange={setPage} />
12
+ return (
13
+ <Pagination
14
+ page={page}
15
+ pageCount={args.pageCount}
16
+ pageRangeDisplayed={args.pageRangeDisplayed}
17
+ size={args.size}
18
+ onChange={setPage}
19
+ />
20
+ )
21
+ }
22
+
23
+ function parsePageFromHash(fallback: number): number {
24
+ const match = window.location.hash.match(/^#page-(\d+)$/)
25
+ return match ? parseInt(match[1], 10) : fallback
26
+ }
27
+
28
+ function LinkPaginationWithState(args: PaginationStoryArgs) {
29
+ const [page, setPage] = useState(() => parsePageFromHash(args.page))
30
+
31
+ useEffect(() => {
32
+ const handleHashChange = () => setPage(parsePageFromHash(args.page))
33
+ window.addEventListener('hashchange', handleHashChange)
34
+ return () => window.removeEventListener('hashchange', handleHashChange)
35
+ }, [args.page])
36
+
37
+ return (
38
+ <div>
39
+ <p style={{ marginBottom: 8, fontSize: 14, color: '#666' }}>
40
+ Current page: {page}
41
+ </p>
42
+ <Pagination
43
+ page={page}
44
+ pageCount={args.pageCount}
45
+ pageRangeDisplayed={args.pageRangeDisplayed}
46
+ size={args.size}
47
+ makeUrl={(p) => `#page-${p}`}
48
+ />
49
+ </div>
50
+ )
8
51
  }
9
52
 
10
53
  export default {
@@ -13,6 +56,7 @@ export default {
13
56
  parameters: {
14
57
  layout: 'centered',
15
58
  },
59
+ render: (args) => <PaginationWithState {...args} />,
16
60
  } satisfies Meta<typeof Pagination>
17
61
 
18
62
  export const Default: StoryObj<typeof Pagination> = {
@@ -20,7 +64,6 @@ export const Default: StoryObj<typeof Pagination> = {
20
64
  page: 5,
21
65
  pageCount: 10,
22
66
  },
23
- render: (args) => <PaginationWithState {...args} />,
24
67
  }
25
68
 
26
69
  export const FirstPage: StoryObj<typeof Pagination> = {
@@ -28,7 +71,6 @@ export const FirstPage: StoryObj<typeof Pagination> = {
28
71
  page: 1,
29
72
  pageCount: 10,
30
73
  },
31
- render: (args) => <PaginationWithState {...args} />,
32
74
  }
33
75
 
34
76
  export const LastPage: StoryObj<typeof Pagination> = {
@@ -36,7 +78,6 @@ export const LastPage: StoryObj<typeof Pagination> = {
36
78
  page: 10,
37
79
  pageCount: 10,
38
80
  },
39
- render: (args) => <PaginationWithState {...args} />,
40
81
  }
41
82
 
42
83
  export const ManyPages: StoryObj<typeof Pagination> = {
@@ -44,14 +85,29 @@ export const ManyPages: StoryObj<typeof Pagination> = {
44
85
  page: 50,
45
86
  pageCount: 103,
46
87
  },
47
- render: (args) => <PaginationWithState {...args} />,
48
88
  }
49
89
 
50
- export const LinkPaginationStory: StoryObj<typeof LinkPagination> = {
90
+ export const SizeS: StoryObj<typeof Pagination> = {
91
+ args: {
92
+ page: 5,
93
+ pageCount: 10,
94
+ size: 'S',
95
+ },
96
+ }
97
+
98
+ export const PageRange5: StoryObj<typeof Pagination> = {
99
+ args: {
100
+ page: 5,
101
+ pageCount: 10,
102
+ pageRangeDisplayed: 5,
103
+ size: 'S',
104
+ },
105
+ }
106
+
107
+ export const LinkPaginationStory: StoryObj<typeof Pagination> = {
51
108
  args: {
52
109
  page: 5,
53
110
  pageCount: 10,
54
- makeUrl: (p) => `#page-${p}`,
55
111
  },
56
- render: (args) => <LinkPagination {...args} />,
112
+ render: (args) => <LinkPaginationWithState {...args} />,
57
113
  }
@@ -0,0 +1,35 @@
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
+ type PaginationContextValue = {
14
+ page: number
15
+ pageCount: number
16
+ size: Size
17
+ isLinkMode: boolean
18
+ makeUrl?: (page: number) => string
19
+ LinkComponent: React.ElementType<LinkComponentProps>
20
+ makeClickHandler: (value: number) => () => void
21
+ }
22
+
23
+ export const PaginationContext = createContext<PaginationContextValue | null>(
24
+ null,
25
+ )
26
+
27
+ export function usePaginationContext(): PaginationContextValue {
28
+ const context = useContext(PaginationContext)
29
+ if (context == null) {
30
+ throw new Error(
31
+ 'Pagination components must be used within a Pagination component',
32
+ )
33
+ }
34
+ return context
35
+ }
@@ -0,0 +1,58 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import { usePaginationWindow } from './helper'
3
+
4
+ describe('usePaginationWindow', () => {
5
+ describe('7 pages or less (no ellipsis)', () => {
6
+ it('returns [1,2,3,4,5,6,7] when page=1, pageCount=10', () => {
7
+ const { result } = renderHook(() => usePaginationWindow(1, 10))
8
+ expect(result.current).toEqual([1, 2, 3, 4, 5, 6, 7])
9
+ })
10
+
11
+ it('returns [1,2,3,4,5,6,7] when page=2, pageCount=10', () => {
12
+ const { result } = renderHook(() => usePaginationWindow(2, 10))
13
+ expect(result.current).toEqual([1, 2, 3, 4, 5, 6, 7])
14
+ })
15
+
16
+ it('returns [1,2,3,4,5,6,7] when page=4, pageCount=10', () => {
17
+ const { result } = renderHook(() => usePaginationWindow(4, 10))
18
+ expect(result.current).toEqual([1, 2, 3, 4, 5, 6, 7])
19
+ })
20
+ })
21
+
22
+ describe('with ellipsis', () => {
23
+ it('returns [1, ..., 4, 5, 6, 7, 8] when page=5, pageCount=100', () => {
24
+ const { result } = renderHook(() => usePaginationWindow(5, 100))
25
+ expect(result.current).toEqual([1, '...', 4, 5, 6, 7, 8])
26
+ })
27
+
28
+ it('returns [1, ..., 96, 97, 98, 99, 100] when page=100, pageCount=100', () => {
29
+ const { result } = renderHook(() => usePaginationWindow(100, 100))
30
+ expect(result.current).toEqual([1, '...', 96, 97, 98, 99, 100])
31
+ })
32
+ })
33
+
34
+ describe('with pageRangeDisplayed specified', () => {
35
+ it('shows first 5 pages when pageRangeDisplayed=5 (no ellipsis)', () => {
36
+ const { result } = renderHook(() => usePaginationWindow(2, 20, 5))
37
+ expect(result.current).toEqual([1, 2, 3, 4, 5])
38
+ })
39
+
40
+ it('shows ellipsis when pageRangeDisplayed=5', () => {
41
+ const { result } = renderHook(() => usePaginationWindow(5, 20, 5))
42
+ expect(result.current).toEqual([1, '...', 5, 6, 7])
43
+ })
44
+ })
45
+
46
+ describe('when arguments change', () => {
47
+ it('updates window when page changes', () => {
48
+ const { result, rerender } = renderHook(
49
+ ({ page, pageCount }) => usePaginationWindow(page, pageCount),
50
+ { initialProps: { page: 2, pageCount: 10 } },
51
+ )
52
+ expect(result.current).toEqual([1, 2, 3, 4, 5, 6, 7])
53
+
54
+ rerender({ page: 5, pageCount: 10 })
55
+ expect(result.current).toEqual([1, '...', 4, 5, 6, 7, 8])
56
+ })
57
+ })
58
+ })
@@ -1,11 +1,13 @@
1
- import { useDebugValue, useMemo } from 'react'
1
+ import { useDebugValue } from 'react'
2
2
  import warning from 'warning'
3
+ import type { PageRangeDisplayed } from './PaginationContext'
3
4
 
4
- export function usePagerWindow(
5
+ export function usePaginationWindow(
5
6
  page: number,
6
7
  pageCount: number,
7
- pageRangeDisplayed = 7,
8
+ pageRangeDisplayed: PageRangeDisplayed = 7,
8
9
  ) {
10
+ 'use memo'
9
11
  // ページャーのリンク生成例:
10
12
  //
11
13
  // < [ 1 ] [*2*] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] >
@@ -29,19 +31,18 @@ export function usePagerWindow(
29
31
  `\`pageCount\` must be integer (${pageCount})`,
30
32
  )
31
33
  warning(
32
- (pageRangeDisplayed | 0) === pageRangeDisplayed,
33
- `\`pageRangeDisplayed\` must be integer (${pageRangeDisplayed})`,
34
+ pageRangeDisplayed === 5 || pageRangeDisplayed === 7,
35
+ `\`pageRangeDisplayed\` must be 5 or 7 (${pageRangeDisplayed})`,
34
36
  )
35
- warning(pageRangeDisplayed > 2, `\`windowSize\` must be greater than 2`)
36
37
  }
37
38
 
38
- const window = useMemo(() => {
39
- const visibleFirstPage = 1
40
- const visibleLastPage = Math.min(
41
- pageCount,
42
- Math.max(page + Math.floor(pageRangeDisplayed / 2), pageRangeDisplayed),
43
- )
39
+ const visibleFirstPage = 1
40
+ const visibleLastPage = Math.min(
41
+ pageCount,
42
+ Math.max(page + Math.floor(pageRangeDisplayed / 2), pageRangeDisplayed),
43
+ )
44
44
 
45
+ const window = (() => {
45
46
  if (visibleLastPage <= pageRangeDisplayed) {
46
47
  // 表示範囲が1-7ページなら省略は無い。
47
48
  return Array.from(
@@ -50,11 +51,11 @@ export function usePagerWindow(
50
51
  )
51
52
  } else {
52
53
  const start = visibleLastPage - (pageRangeDisplayed - 1) + 2
54
+ // 表示範囲が1-7ページを超えるなら、
55
+ // - 1ページ目は固定で表示する
56
+ // - 2ページ目から現在のページの直前までは省略する
53
57
  return [
54
- // 表示範囲が1-7ページを超えるなら、
55
- // - 1ページ目は固定で表示する
56
58
  visibleFirstPage,
57
- // - 2ページ目から現在のページの直前までは省略する
58
59
  '...' as const,
59
60
  ...Array.from(
60
61
  { length: 1 + visibleLastPage - start },
@@ -62,7 +63,7 @@ export function usePagerWindow(
62
63
  ),
63
64
  ]
64
65
  }
65
- }, [page, pageCount, pageRangeDisplayed])
66
+ })()
66
67
 
67
68
  useDebugValue(window)
68
69
 
@@ -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;