|
| 1 | +import { spy } from 'sinon'; |
1 | 2 | import InputAdornment, { InputAdornmentProps } from '@mui/material/InputAdornment'; |
2 | 3 | import { DateField } from '@mui/x-date-pickers/DateField'; |
3 | 4 | import { screen } from '@mui/internal-test-utils'; |
@@ -40,6 +41,69 @@ describe('<DateField />', () => { |
40 | 41 | }); |
41 | 42 | }); |
42 | 43 |
|
| 44 | + describe('slotProps.textField focus/blur behavior', () => { |
| 45 | + it('should not call `slotProps.textField.onBlur` when focus enters the field via tab', async () => { |
| 46 | + const onBlur = spy(); |
| 47 | + const view = render(<DateField slotProps={{ textField: { onBlur } }} />); |
| 48 | + |
| 49 | + // Tabbing into the field moves focus to the PickersSectionList root (tabIndex=0) |
| 50 | + // first, then programmatically to section 0. The transient root blur must not |
| 51 | + // dispatch the user's onBlur callback. |
| 52 | + await view.user.tab(); |
| 53 | + |
| 54 | + expect(onBlur.callCount).to.equal(0); |
| 55 | + }); |
| 56 | + |
| 57 | + it('should call `slotProps.textField.onFocus` only once when focus enters the field via tab', async () => { |
| 58 | + const onFocus = spy(); |
| 59 | + const view = render(<DateField slotProps={{ textField: { onFocus } }} />); |
| 60 | + |
| 61 | + // Tabbing into the field fires a focus on the root and then on section 0. |
| 62 | + // Only the first focus (from outside the field) should reach the user. |
| 63 | + await view.user.tab(); |
| 64 | + |
| 65 | + expect(onFocus.callCount).to.equal(1); |
| 66 | + }); |
| 67 | + |
| 68 | + it('should call `slotProps.textField.onFocus` only once when focus is applied programmatically via autoFocus', () => { |
| 69 | + const onFocus = spy(); |
| 70 | + // `autoFocus` triggers a programmatic `.focus()` on section 0 from a mount effect, |
| 71 | + // which can produce a focus event with `relatedTarget === null`. The user callback |
| 72 | + // must still fire exactly once. |
| 73 | + render(<DateField autoFocus slotProps={{ textField: { onFocus } }} />); |
| 74 | + |
| 75 | + expect(onFocus.callCount).to.equal(1); |
| 76 | + }); |
| 77 | + |
| 78 | + it('should call `slotProps.textField.onBlur` when focus leaves the field via tab', async () => { |
| 79 | + const onBlur = spy(); |
| 80 | + const view = render(<DateField slotProps={{ textField: { onBlur } }} />); |
| 81 | + |
| 82 | + await view.user.tab(); |
| 83 | + expect(onBlur.callCount).to.equal(0); |
| 84 | + |
| 85 | + // Tab out of the field (to the document body or next focusable). |
| 86 | + await view.user.tab(); |
| 87 | + expect(onBlur.callCount).to.equal(1); |
| 88 | + }); |
| 89 | + |
| 90 | + it('should not fire `slotProps.textField.onBlur` or `onFocus` when focus moves between sections', async () => { |
| 91 | + const onBlur = spy(); |
| 92 | + const onFocus = spy(); |
| 93 | + const view = render(<DateField slotProps={{ textField: { onBlur, onFocus } }} />); |
| 94 | + |
| 95 | + await view.user.tab(); |
| 96 | + const blurCallCountAfterTabIn = onBlur.callCount; |
| 97 | + const focusCallCountAfterTabIn = onFocus.callCount; |
| 98 | + |
| 99 | + // Navigate across sections inside the field. |
| 100 | + await view.user.keyboard('[ArrowRight][ArrowRight][ArrowLeft]'); |
| 101 | + |
| 102 | + expect(onBlur.callCount).to.equal(blurCallCountAfterTabIn); |
| 103 | + expect(onFocus.callCount).to.equal(focusCallCountAfterTabIn); |
| 104 | + }); |
| 105 | + }); |
| 106 | + |
43 | 107 | describe('slotProps.inputAdornment behavior', () => { |
44 | 108 | function CustomInputAdornment(props: InputAdornmentProps) { |
45 | 109 | const { children, ...other } = props; |
|
0 commit comments