11import styled from '@emotion/styled' ;
2+ import React , { useCallback } from 'react' ;
23import { textCss } from './textCss' ;
34import { TextSize , useTextSize } from './textSize' ;
45import { getTransform } from './transform' ;
56import { noop } from './util/noop' ;
6- import { useCallback } from 'react' ;
77import { clsx } from 'clsx' ;
88
99/**
@@ -133,6 +133,17 @@ export interface SeatProps {
133133 x ?: number ;
134134 /** Y position of the seat in seatmap units. Defaults to `0`. */
135135 y ?: number ;
136+ /**
137+ * Tab index for keyboard navigation. When provided, overrides the default behaviour of
138+ * `0` for enabled seats and `-1` for disabled seats. Used by {@link SeatmapLayout} to
139+ * implement the roving tabindex pattern.
140+ */
141+ tabIndex ?: number ;
142+ /**
143+ * Focus event handler. Used by {@link SeatmapLayout} to sync the roving focus position
144+ * when a user clicks directly on the seat.
145+ */
146+ onFocus ?: React . FocusEventHandler < SVGGElement > ;
136147}
137148
138149/**
@@ -157,63 +168,75 @@ export interface SeatProps {
157168 *
158169 * @public
159170 */
160- export const Seat = ( {
161- x = 0 ,
162- y = 0 ,
163- name,
164- ariaLabel,
165- hideName = false ,
166- color,
167- disabled = false ,
168- onClick = noop ,
169- onDisabledClick = noop ,
170- active = false ,
171- shape = SeatShape . SQUARE ,
172- } : SeatProps ) => {
173- const textSize = useTextSize ( ( name ?. length ?? 0 ) > 2 ? TextSize . SMALL : TextSize . NORMAL ) ;
174- const textTransform = getTransform ( x , y ) ;
175- const handleClick = useCallback (
176- ( ) => ( disabled ? onDisabledClick : onClick ) ( ) ,
177- [ disabled , onClick , onDisabledClick ] ,
178- ) ;
179- const handleKeyDown = useCallback (
180- ( event : React . KeyboardEvent ) => {
181- if ( event . key === 'Enter' || event . key === ' ' ) {
182- event . preventDefault ( ) ;
183- handleClick ( ) ;
184- }
171+ export const Seat = React . forwardRef < SVGGElement , SeatProps > (
172+ (
173+ {
174+ x = 0 ,
175+ y = 0 ,
176+ name,
177+ ariaLabel,
178+ hideName = false ,
179+ color,
180+ disabled = false ,
181+ onClick = noop ,
182+ onDisabledClick = noop ,
183+ active = false ,
184+ shape = SeatShape . SQUARE ,
185+ tabIndex : tabIndexProp ,
186+ onFocus,
185187 } ,
186- [ handleClick ] ,
187- ) ;
188- const ShapeComponent = shape === SeatShape . CIRCLE ? CircularSeat : SquareSeat ;
189- const transform = getTransform ( x + 2.5 , y + 2.5 ) ;
190- return (
191- < StyledSeat
192- className = { clsx ( { nameHidden : hideName , clickable : onClick !== noop && ! disabled , active : active } ) }
193- onClick = { handleClick }
194- onKeyDown = { handleKeyDown }
195- tabIndex = { disabled ? - 1 : 0 }
196- role = "button"
197- aria-label = { ariaLabel ?? name ?? 'Unnamed seat' }
198- aria-pressed = { active }
199- aria-disabled = { disabled }
200- >
201- < ShapeComponent
202- transform = { transform }
203- fill = { disabled ? '#cccccc' : color }
204- />
205- { name !== undefined ? (
206- < Name
207- transform = { textTransform }
208- x = "5"
209- y = "5"
210- className = "name"
211- style = { textSize === TextSize . SMALL ? { fontSize : 4 } : undefined }
212- aria-hidden = { true }
213- >
214- { name }
215- </ Name >
216- ) : undefined }
217- </ StyledSeat >
218- ) ;
219- } ;
188+ ref ,
189+ ) => {
190+ const textSize = useTextSize ( ( name ?. length ?? 0 ) > 2 ? TextSize . SMALL : TextSize . NORMAL ) ;
191+ const textTransform = getTransform ( x , y ) ;
192+ const handleClick = useCallback (
193+ ( ) => ( disabled ? onDisabledClick : onClick ) ( ) ,
194+ [ disabled , onClick , onDisabledClick ] ,
195+ ) ;
196+ const handleKeyDown = useCallback (
197+ ( event : React . KeyboardEvent ) => {
198+ if ( event . key === 'Enter' || event . key === ' ' ) {
199+ event . preventDefault ( ) ;
200+ handleClick ( ) ;
201+ }
202+ } ,
203+ [ handleClick ] ,
204+ ) ;
205+ const ShapeComponent = shape === SeatShape . CIRCLE ? CircularSeat : SquareSeat ;
206+ const transform = getTransform ( x + 2.5 , y + 2.5 ) ;
207+ const resolvedTabIndex = tabIndexProp !== undefined ? tabIndexProp : disabled ? - 1 : 0 ;
208+ return (
209+ < StyledSeat
210+ ref = { ref }
211+ className = { clsx ( { nameHidden : hideName , clickable : onClick !== noop && ! disabled , active : active } ) }
212+ onClick = { handleClick }
213+ onKeyDown = { handleKeyDown }
214+ onFocus = { onFocus }
215+ tabIndex = { resolvedTabIndex }
216+ role = "button"
217+ aria-label = { ariaLabel ?? name ?? 'Unnamed seat' }
218+ aria-pressed = { active }
219+ aria-disabled = { disabled }
220+ >
221+ < ShapeComponent
222+ transform = { transform }
223+ fill = { disabled ? '#cccccc' : color }
224+ />
225+ { name !== undefined ? (
226+ < Name
227+ transform = { textTransform }
228+ x = "5"
229+ y = "5"
230+ className = "name"
231+ style = { textSize === TextSize . SMALL ? { fontSize : 4 } : undefined }
232+ aria-hidden = { true }
233+ >
234+ { name }
235+ </ Name >
236+ ) : undefined }
237+ </ StyledSeat >
238+ ) ;
239+ } ,
240+ ) ;
241+
242+ Seat . displayName = 'Seat' ;
0 commit comments