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

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.
@@ -134,23 +134,44 @@
134
134
  }
135
135
  }
136
136
 
137
- /* hover で表示。キーボード操作でも到達できるよう focus-within でも表示する。 */
137
+ /* hover で表示。キーボード操作でも到達できるよう、キーボード由来のフォーカスが
138
+ カルーセル内にある間も表示する(:focus-within だとクリックで残ったフォーカスでも
139
+ 表示され続けるため、react-aria が付与する data 属性で判定する)。 */
138
140
  .charcoal-carousel:hover .charcoal-carousel__navigation,
139
- .charcoal-carousel:focus-within .charcoal-carousel__navigation {
141
+ .charcoal-carousel[data-focus-visible-within] .charcoal-carousel__navigation {
140
142
  opacity: 1;
141
143
  }
142
144
 
143
145
  .charcoal-carousel__navigation__item {
144
146
  pointer-events: auto;
145
147
 
148
+ /* ヒットエリアをグリッド端列(= mask のフェード帯と同じ 72px)全体へ広げる。
149
+ 擬似要素上のクリックは元の button に届くため、帯のどこを押してもページ送りになる。
150
+ position: absolute の基準は positioned な .charcoal-carousel__navigation(inset: 0)。 */
151
+ &::after {
152
+ content: '';
153
+ position: absolute;
154
+ top: 0;
155
+ bottom: 0;
156
+ width: 72px;
157
+ }
158
+
146
159
  &[data-direction='prev'] {
147
160
  grid-column: 1;
148
161
  justify-self: center;
162
+
163
+ &::after {
164
+ left: 0;
165
+ }
149
166
  }
150
167
 
151
168
  &[data-direction='next'] {
152
169
  grid-column: 3;
153
170
  justify-self: center;
171
+
172
+ &::after {
173
+ right: 0;
174
+ }
154
175
  }
155
176
  }
156
177
 
@@ -634,6 +634,58 @@ describe('Carousel', () => {
634
634
  })
635
635
  })
636
636
 
637
+ describe('navigation button visibility (focus modality)', () => {
638
+ // jsdom はジオメトリが全て 0 で Next ボタンが disabled(=フォーカス不可)の
639
+ // ままになるため、スクロール余地をモックして有効化してからフォーカスする。
640
+ function renderWithFocusableNext() {
641
+ render(<Carousel size="M">{slides}</Carousel>)
642
+ const scroller = getScroller()
643
+ mockScrollerGeometry(scroller, { scrollLeft: 0 })
644
+ scroller.scrollBy = vi.fn()
645
+ act(() => {
646
+ fireEvent.scroll(scroller)
647
+ })
648
+ const next = screen.getByRole('button', { name: 'Next' })
649
+ expect(next).not.toBeDisabled()
650
+ return next
651
+ }
652
+
653
+ // react-aria は document 上の keydown / pointerdown でフォーカスの由来
654
+ // (keyboard / pointer)を判定するため、focus() の前にイベントを発火して
655
+ // モダリティを切り替える。
656
+ it('does not mark focus-visible when a button is focused by pointer', () => {
657
+ const next = renderWithFocusableNext()
658
+ fireEvent.pointerDown(next)
659
+ fireEvent.mouseDown(next)
660
+ act(() => next.focus())
661
+ fireEvent.click(next)
662
+ expect(document.activeElement).toBe(next)
663
+ expect(screen.getByRole('region')).not.toHaveAttribute(
664
+ 'data-focus-visible-within',
665
+ )
666
+ })
667
+
668
+ it('marks focus-visible when a button is focused by keyboard', () => {
669
+ const next = renderWithFocusableNext()
670
+ fireEvent.keyDown(document.body, { key: 'Tab' })
671
+ act(() => next.focus())
672
+ expect(screen.getByRole('region')).toHaveAttribute(
673
+ 'data-focus-visible-within',
674
+ 'true',
675
+ )
676
+ })
677
+
678
+ it('clears focus-visible when focus leaves the carousel', () => {
679
+ const next = renderWithFocusableNext()
680
+ fireEvent.keyDown(document.body, { key: 'Tab' })
681
+ act(() => next.focus())
682
+ act(() => next.blur())
683
+ expect(screen.getByRole('region')).not.toHaveAttribute(
684
+ 'data-focus-visible-within',
685
+ )
686
+ })
687
+ })
688
+
637
689
  describe('SSR (server rendering)', () => {
638
690
  // jsdom では window が定義されているため renderToString 時に
639
691
  // 「useLayoutEffect does nothing on the server」警告が出る(実 SSR=window 無しでは
@@ -208,8 +208,14 @@ const Carousel = forwardRef<CarouselHandlerRef, CarouselProps>(function Render(
208
208
  isFocusVisible: scrollerFocusVisible,
209
209
  } = useFocusRing()
210
210
 
211
+ // ナビゲーションボタン表示用。クリックで残留したフォーカス(pointer 由来)では
212
+ // 表示し続けないよう、キーボード由来のフォーカスのみを検知する。
213
+ const { focusProps: rootFocusProps, isFocusVisible: rootFocusVisible } =
214
+ useFocusRing({ within: true })
215
+
211
216
  return (
212
217
  <div
218
+ {...rootFocusProps}
213
219
  className={className}
214
220
  data-size={size}
215
221
  data-has-gradient={hasGradient}
@@ -219,6 +225,7 @@ const Carousel = forwardRef<CarouselHandlerRef, CarouselProps>(function Render(
219
225
  data-scroll-snap-align={snapAlign}
220
226
  data-can-prev={canPrev}
221
227
  data-can-next={canNext}
228
+ data-focus-visible-within={rootFocusVisible || undefined}
222
229
  role="region"
223
230
  aria-roledescription="carousel"
224
231
  aria-label="Carousel"