@charcoal-ui/react 6.0.0-rc.6 → 6.0.0-rc.7

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.
@@ -16,25 +16,18 @@ const makeSampleImages = (style: CSSProperties) =>
16
16
  />
17
17
  ))
18
18
 
19
- // M 用: スライド間隔はコンポーネントが所有しないので、利用者側の margin で注入する。
20
- // width: 100% margin の併用はラッパーとの循環サイズ解決で間隔が潰れるため、明示サイズにする。
21
- const spacedImages = makeSampleImages({
22
- width: 280,
23
- height: 210,
24
- marginInlineEnd: 24,
25
- })
19
+ // M 用: スライド間隔は gap prop で注入する(既定 args の gap: 24)。
20
+ const spacedImages = makeSampleImages({ width: 280, height: 210 })
26
21
  // S 用: 1 枚全幅ページングのため間隔なし
27
22
  const fullWidthImages = makeSampleImages({ width: '100%', height: '100%' })
28
23
 
29
24
  // 横幅が確定したスロット(defaultScroll の初期位置計算と 0.75 ページ送りを確認するため)。
30
- // スライド間隔はコンポーネントが所有しないので、利用者側の margin で注入する。
31
25
  const numberedSlides = Array.from({ length: 20 }, (_, i) => (
32
26
  <div
33
27
  key={`num-${i + 1}`}
34
28
  style={{
35
29
  width: 200,
36
30
  height: 120,
37
- marginInlineEnd: 24,
38
31
  display: 'flex',
39
32
  alignItems: 'center',
40
33
  justifyContent: 'center',
@@ -60,6 +53,7 @@ export default {
60
53
  hasGradient: false,
61
54
  fullWidth: false,
62
55
  scrollStep: 0.75,
56
+ gap: 24,
63
57
  },
64
58
  argTypes: {
65
59
  children: { control: false },
@@ -80,6 +74,10 @@ export default {
80
74
  // Controls では数値(比率)のみ操作可能。関数形式は ScrollStepFunction story を参照。
81
75
  control: { type: 'range', min: 0.1, max: 1.5, step: 0.05 },
82
76
  },
77
+ gap: {
78
+ // number は px。'1rem' などの CSS 値文字列も渡せる。
79
+ control: { type: 'number' },
80
+ },
83
81
  },
84
82
  } satisfies Meta<typeof Carousel>
85
83
 
@@ -88,7 +86,7 @@ export const SizeM: StoryObj<typeof Carousel> = {
88
86
  }
89
87
 
90
88
  export const SizeS: StoryObj<typeof Carousel> = {
91
- args: { size: 'S', children: fullWidthImages },
89
+ args: { size: 'S', children: fullWidthImages, gap: 0 },
92
90
  }
93
91
 
94
92
  export const WithGradient: StoryObj<typeof Carousel> = {
@@ -100,7 +98,12 @@ export const FullWidth: StoryObj<typeof Carousel> = {
100
98
  }
101
99
 
102
100
  export const NavigationButtonsOnSizeS: StoryObj<typeof Carousel> = {
103
- args: { size: 'S', navigationButtons: true, children: fullWidthImages },
101
+ args: {
102
+ size: 'S',
103
+ navigationButtons: true,
104
+ children: fullWidthImages,
105
+ gap: 0,
106
+ },
104
107
  }
105
108
 
106
109
  export const IndicatorOnSizeM: StoryObj<typeof Carousel> = {
@@ -140,7 +143,6 @@ const darkTiles = Array.from({ length: 10 }, (_, i) => (
140
143
  style={{
141
144
  width: 220,
142
145
  height: 140,
143
- marginInlineEnd: 24,
144
146
  display: 'flex',
145
147
  alignItems: 'center',
146
148
  justifyContent: 'center',
@@ -92,6 +92,30 @@ describe('Carousel', () => {
92
92
  })
93
93
  })
94
94
 
95
+ describe('gap', () => {
96
+ it('sets --charcoal-carousel-gap in px when gap is a number', () => {
97
+ const { container } = render(<Carousel gap={24}>{slides}</Carousel>)
98
+ const root = container.querySelector('.charcoal-carousel') as HTMLElement
99
+ expect(root.style.getPropertyValue('--charcoal-carousel-gap')).toBe(
100
+ '24px',
101
+ )
102
+ })
103
+
104
+ it('passes string gap values through as-is', () => {
105
+ const { container } = render(<Carousel gap="1rem">{slides}</Carousel>)
106
+ const root = container.querySelector('.charcoal-carousel') as HTMLElement
107
+ expect(root.style.getPropertyValue('--charcoal-carousel-gap')).toBe(
108
+ '1rem',
109
+ )
110
+ })
111
+
112
+ it('does not set the custom property when gap is not specified', () => {
113
+ const { container } = render(<Carousel>{slides}</Carousel>)
114
+ const root = container.querySelector('.charcoal-carousel') as HTMLElement
115
+ expect(root.style.getPropertyValue('--charcoal-carousel-gap')).toBe('')
116
+ })
117
+ })
118
+
95
119
  describe('Size M (default)', () => {
96
120
  it('shows navigation buttons', () => {
97
121
  render(<Carousel size="M">{slides}</Carousel>)
@@ -7,9 +7,11 @@ import {
7
7
  memo,
8
8
  useCallback,
9
9
  useImperativeHandle,
10
+ useMemo,
10
11
  useRef,
11
12
  useState,
12
13
  useSyncExternalStore,
14
+ type CSSProperties,
13
15
  type ReactNode,
14
16
  } from 'react'
15
17
  import { mergeProps, useFocusRing, useKeyboard } from 'react-aria'
@@ -67,6 +69,8 @@ export type CarouselProps = Readonly<{
67
69
  onResize?: (width: number) => void
68
70
  onScrollStateChange?: (canScroll: boolean) => void
69
71
  defaultScroll?: { align?: ScrollAlign; offset?: number }
72
+ // スライド間隔。number は px、string は CSS 値をそのまま使う。未指定は間隔なし。
73
+ gap?: number | string
70
74
  // 1 直接子要素 = 1 スライド(react-sandbox 互換)。
71
75
  children: ReactNode
72
76
  }>
@@ -147,6 +151,7 @@ const Carousel = forwardRef<CarouselHandlerRef, CarouselProps>(function Render(
147
151
  onResize,
148
152
  onScrollStateChange,
149
153
  defaultScroll: { align = 'left', offset = 0 } = {},
154
+ gap,
150
155
  children,
151
156
  ...props
152
157
  }: CarouselProps,
@@ -213,10 +218,22 @@ const Carousel = forwardRef<CarouselHandlerRef, CarouselProps>(function Render(
213
218
  const { focusProps: rootFocusProps, isFocusVisible: rootFocusVisible } =
214
219
  useFocusRing({ within: true })
215
220
 
221
+ // gap 宣言自体は index.css 側に置き、ここでは CSS 変数の値だけを注入する。
222
+ const gapStyle = useMemo(
223
+ () =>
224
+ ({
225
+ ...(gap != null && {
226
+ '--charcoal-carousel-gap': typeof gap === 'number' ? `${gap}px` : gap,
227
+ }),
228
+ }) satisfies CSSProperties,
229
+ [gap],
230
+ )
231
+
216
232
  return (
217
233
  <div
218
234
  {...rootFocusProps}
219
235
  className={className}
236
+ style={gapStyle}
220
237
  data-size={size}
221
238
  data-has-gradient={hasGradient}
222
239
  data-full-width={fullWidth}
@@ -0,0 +1,38 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react'
2
+ import { forwardRef } from 'react'
3
+ import TagItem from '.'
4
+
5
+ describe('TagItem', () => {
6
+ it('preserves link props on a custom link component', () => {
7
+ const NextLink = forwardRef<
8
+ HTMLAnchorElement,
9
+ React.ComponentPropsWithoutRef<'a'>
10
+ >(function NextLink({ children, ...props }, ref) {
11
+ return (
12
+ <a ref={ref} {...props}>
13
+ {children}
14
+ </a>
15
+ )
16
+ })
17
+
18
+ const onClick = vi.fn()
19
+
20
+ render(
21
+ <TagItem
22
+ component={NextLink}
23
+ href="/tags/girl"
24
+ label="#girl"
25
+ target="_blank"
26
+ onClick={onClick}
27
+ />,
28
+ )
29
+
30
+ const link = screen.getByRole('link')
31
+
32
+ expect(link).toHaveAttribute('href', '/tags/girl')
33
+ expect(link).toHaveAttribute('target', '_blank')
34
+
35
+ fireEvent.click(link)
36
+ expect(onClick).toHaveBeenCalledTimes(1)
37
+ })
38
+ })
@@ -61,6 +61,7 @@ const TagItem = forwardRef<HTMLAnchorElement, TagItemProps>(
61
61
 
62
62
  const { linkProps } = useLink(
63
63
  {
64
+ ...props,
64
65
  isDisabled: disabled,
65
66
  elementType: typeof Component === 'string' ? Component : 'a',
66
67
  },
@@ -30,6 +30,10 @@
30
30
  box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
31
31
  }
32
32
 
33
+ .charcoal-text-area-container:has([data-no-bottom-padding='true']) {
34
+ height: calc(22px * (var(--charcoal-text-area-rows) + 1) + 18px);
35
+ }
36
+
33
37
  .charcoal-text-area-container:not([aria-disabled='true']):hover {
34
38
  background-color: var(--charcoal-color-container-secondary-hover-a);
35
39
  }
@@ -47,13 +51,26 @@
47
51
  box-sizing: border-box;
48
52
 
49
53
  /* Prevent zooming for iOS Safari */
54
+ --charcoal-textarea-safari-prevent-zoom-scale: 0.875;
55
+ --charcoal-textarea-line-height: calc(
56
+ 22px / var(--charcoal-textarea-safari-prevent-zoom-scale)
57
+ );
58
+ --charcoal-textarea-padding-y: calc(
59
+ 8px / var(--charcoal-textarea-safari-prevent-zoom-scale)
60
+ );
61
+ --charcoal-textarea-padding-x: calc(
62
+ 9px / var(--charcoal-textarea-safari-prevent-zoom-scale)
63
+ );
50
64
  transform-origin: top left;
51
- transform: scale(0.875);
52
- width: calc(100% / 0.875);
53
- font-size: calc(14px / 0.875);
54
- line-height: calc(22px / 0.875);
55
- padding: calc(9px / 0.875) calc(8px / 0.875);
56
- height: calc(22px / 0.875 * var(--charcoal-text-area-rows) + 20px);
65
+ transform: scale(var(--charcoal-textarea-safari-prevent-zoom-scale));
66
+ width: calc(100% / var(--charcoal-textarea-safari-prevent-zoom-scale));
67
+ font-size: calc(14px / var(--charcoal-textarea-safari-prevent-zoom-scale));
68
+ line-height: var(--charcoal-textarea-line-height);
69
+ padding: var(--charcoal-textarea-padding-y) var(--charcoal-textarea-padding-x);
70
+ height: calc(
71
+ var(--charcoal-textarea-line-height) * var(--charcoal-text-area-rows) +
72
+ var(--charcoal-textarea-padding-y) * 2
73
+ );
57
74
 
58
75
  /* Display box-shadow for iOS Safari */
59
76
  appearance: none;
@@ -61,8 +78,12 @@
61
78
  }
62
79
 
63
80
  .charcoal-text-area-textarea[data-no-bottom-padding='true'] {
64
- padding: calc(9px / 0.875) calc(8px / 0.875) 0;
65
- height: calc(22px / 0.875 * (var(--charcoal-text-area-rows) - 1) + 9px);
81
+ padding: var(--charcoal-textarea-padding-y)
82
+ var(--charcoal-textarea-padding-x) 0;
83
+ height: calc(
84
+ var(--charcoal-textarea-line-height) * var(--charcoal-text-area-rows) +
85
+ var(--charcoal-textarea-padding-y)
86
+ );
66
87
  }
67
88
 
68
89
  .charcoal-text-area-textarea::placeholder {
@@ -32,6 +32,10 @@
32
32
  box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
33
33
  }
34
34
 
35
+ &:has([data-no-bottom-padding='true']) {
36
+ height: calc(22px * (var(--charcoal-text-area-rows) + 1) + 18px);
37
+ }
38
+
35
39
  &:not([aria-disabled='true']):hover {
36
40
  background-color: var(--charcoal-color-container-secondary-hover-a);
37
41
  }
@@ -50,13 +54,27 @@
50
54
  box-sizing: border-box;
51
55
 
52
56
  /* Prevent zooming for iOS Safari */
57
+ --charcoal-textarea-safari-prevent-zoom-scale: 0.875;
58
+ --charcoal-textarea-line-height: calc(
59
+ 22px / var(--charcoal-textarea-safari-prevent-zoom-scale)
60
+ );
61
+ --charcoal-textarea-padding-y: calc(
62
+ 8px / var(--charcoal-textarea-safari-prevent-zoom-scale)
63
+ );
64
+ --charcoal-textarea-padding-x: calc(
65
+ 9px / var(--charcoal-textarea-safari-prevent-zoom-scale)
66
+ );
67
+
53
68
  transform-origin: top left;
54
- transform: scale(0.875);
55
- width: calc(100% / 0.875);
56
- font-size: calc(14px / 0.875);
57
- line-height: calc(22px / 0.875);
58
- padding: calc(9px / 0.875) calc(8px / 0.875);
59
- height: calc(22px / 0.875 * var(--charcoal-text-area-rows) + 20px);
69
+ transform: scale(var(--charcoal-textarea-safari-prevent-zoom-scale));
70
+ width: calc(100% / var(--charcoal-textarea-safari-prevent-zoom-scale));
71
+ font-size: calc(14px / var(--charcoal-textarea-safari-prevent-zoom-scale));
72
+ line-height: var(--charcoal-textarea-line-height);
73
+ padding: var(--charcoal-textarea-padding-y) var(--charcoal-textarea-padding-x);
74
+ height: calc(
75
+ var(--charcoal-textarea-line-height) * var(--charcoal-text-area-rows) +
76
+ var(--charcoal-textarea-padding-y) * 2
77
+ );
60
78
 
61
79
  /* Display box-shadow for iOS Safari */
62
80
  appearance: none;
@@ -64,8 +82,12 @@
64
82
  background: none;
65
83
 
66
84
  &[data-no-bottom-padding='true'] {
67
- padding: calc(9px / 0.875) calc(8px / 0.875) 0;
68
- height: calc(22px / 0.875 * (var(--charcoal-text-area-rows) - 1) + 9px);
85
+ padding: var(--charcoal-textarea-padding-y)
86
+ var(--charcoal-textarea-padding-x) 0;
87
+ height: calc(
88
+ var(--charcoal-textarea-line-height) * var(--charcoal-text-area-rows) +
89
+ var(--charcoal-textarea-padding-y)
90
+ );
69
91
  }
70
92
 
71
93
  &::placeholder {
@@ -206,7 +206,7 @@ const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
206
206
  aria-invalid={invalid === true}
207
207
  ref={containerRef}
208
208
  style={{
209
- '--charcoal-text-area-rows': `${showCount ? rows + 1 : rows}`,
209
+ '--charcoal-text-area-rows': rows,
210
210
  }}
211
211
  >
212
212
  <textarea