@@ -141,6 +141,7 @@ interface DateInputGroupProps {
141141 type : Exclude < Segment [ "type" ] , "literal" > ,
142142 digit : number ,
143143 replace ?: boolean ,
144+ digitCount ?: number ,
144145 ) => { advance : boolean } ;
145146 setDayPeriod : ( pm : boolean ) => void ;
146147 clearSegment : ( type : Exclude < Segment [ "type" ] , "literal" > ) => void ;
@@ -154,6 +155,8 @@ interface DateInputGroupProps {
154155 describedById ?: string ;
155156 className ?: string ;
156157 trigger ?: React . ReactNode ;
158+ /** Ref to the group element — used to anchor the popover to the whole field. */
159+ groupRef ?: React . Ref < HTMLDivElement > ;
157160}
158161
159162export function DateInputGroup ( {
@@ -171,11 +174,13 @@ export function DateInputGroup({
171174 describedById,
172175 className,
173176 trigger,
177+ groupRef,
174178} : DateInputGroupProps ) {
175179 const editableRefs = React . useRef < ( HTMLDivElement | null ) [ ] > ( [ ] ) ;
176- // The segment that just gained focus and hasn't received a digit yet. Its
177- // first digit replaces the current value rather than accumulating onto it.
178- const freshSegmentRef = React . useRef < Segment [ "type" ] | null > ( null ) ;
180+ // Digits typed into the currently-focused segment this session. Reset on
181+ // focus; the first digit (count 0) replaces, and the count decides when the
182+ // segment is "full" and should auto-advance.
183+ const typedCountRef = React . useRef ( 0 ) ;
179184
180185 React . useEffect ( ( ) => {
181186 if ( autoFocus && ! isDisabled ) editableRefs . current [ 0 ] ?. focus ( ) ;
@@ -249,16 +254,17 @@ export function DateInputGroup({
249254
250255 if ( / ^ \d $ / . test ( e . key ) ) {
251256 e . preventDefault ( ) ;
252- const replace = freshSegmentRef . current === type ;
253- freshSegmentRef . current = null ;
254- const { advance } = setDigit ( type , Number ( e . key ) , replace ) ;
257+ const count = typedCountRef . current ;
258+ typedCountRef . current = count + 1 ;
259+ const { advance } = setDigit ( type , Number ( e . key ) , count === 0 , count + 1 ) ;
255260 if ( advance ) focusEditable ( editableIndex + 1 ) ;
256261 }
257262 } ;
258263
259264 return (
260265 // Deliberate APG date-field pattern: a labelled group wrapping spinbutton segments.
261266 < div
267+ ref = { groupRef }
262268 role = "group"
263269 data-slot = "date-picker-group"
264270 aria-labelledby = { labelId }
@@ -309,7 +315,7 @@ export function DateInputGroup({
309315 aria-valuenow = { segment . value }
310316 aria-valuetext = { segment . isPlaceholder ? "Empty" : segment . text }
311317 onFocus = { ( ) => {
312- freshSegmentRef . current = segment . type ;
318+ typedCountRef . current = 0 ;
313319 } }
314320 onKeyDown = { ( e ) => handleKeyDown ( e , segment , editableIndex ) }
315321 className = { cn (
@@ -571,14 +577,27 @@ interface DatePopoverProps {
571577 field : React . ReactNode ;
572578 children : React . ReactNode ;
573579 ariaLabel ?: string ;
580+ /**
581+ * Element to position the calendar against. Defaults to the trigger; pass the
582+ * field group so the calendar aligns to the field's edge (not the icon),
583+ * overlapping it horizontally and shifting inward near the viewport edge.
584+ */
585+ anchor ?: React . RefObject < HTMLElement | null > ;
574586}
575587
576- export function DatePopover ( { open, onOpenChange, field, children, ariaLabel } : DatePopoverProps ) {
588+ export function DatePopover ( {
589+ open,
590+ onOpenChange,
591+ field,
592+ children,
593+ ariaLabel,
594+ anchor,
595+ } : DatePopoverProps ) {
577596 return (
578597 < Popover . Root open = { open } onOpenChange = { onOpenChange } >
579598 { field }
580599 < Popover . Portal >
581- < Popover . Positioner sideOffset = { 4 } side = "bottom" align = "start" >
600+ < Popover . Positioner anchor = { anchor } sideOffset = { 4 } side = "bottom" align = "start" >
582601 { /* APG date-picker dialog pattern — the popup is a labelled dialog. */ }
583602 < Popover . Popup
584603 role = "dialog"
0 commit comments