-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Components: Reuse existing useControlledValue
hook in Calendar components
#70655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Components: Reuse existing useControlledValue
hook in Calendar components
#70655
Conversation
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
defaultSelected: new Date( 1751472000000 ), | ||
selected: new Date( 1751558400000 ), | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed TS errors. I'm not sure why these TS errors weren't caught in the previous PR.
gutenberg/packages/components/src/calendar/types.ts
Lines 310 to 318 in 9504e40
interface SingleProps { | |
/** The selected date. */ | |
selected?: Date | undefined | null; | |
/** Event handler when a day is selected. */ | |
onSelect?: OnSelectHandler< Date | undefined >; | |
/** The default selected date (for uncontrolled usage). */ | |
defaultSelected?: Date; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which errors? AFAIK it's fine for DateCalendar
not to have those props defined. Besides, it wouldn't make sense to define both defaultSelected
and selected
at the same time — one is for uncontrolled usage, the other one for controlled usage.
We should be able to undo these changes without causing TS errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right! Previously, I encountered TS errors that stopped me from committing the changes. But now, I don't see any errors. I'll undo the changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted e66276b
@@ -48,7 +51,7 @@ export const DateCalendar = ( { | |||
{ ...props } | |||
mode="single" | |||
numberOfMonths={ clampNumberOfMonths( numberOfMonths ) } | |||
selected={ selected } | |||
selected={ selected ?? undefined } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, the calendar's useControlledValue
hook internally converted the empty value to undefined
. I don't think this should be added to the utils useControlledValue, so I updated the calendar's implementation to handle this case.
const value = ( hasValue ? valueProp : state ) ?? undefined; |
66c8e75
to
24b85da
Compare
- Updated the `onChange` prop to accept additional arguments for better flexibility. - Introduced a new `uncontrolledSetValue` function using `useCallback` to streamline state updates and onChange calls in uncontrolled mode. - Improved code clarity by separating controlled and uncontrolled mode logic. This change enhances the usability of the hook in various scenarios.
…hook - Updated imports of `useControlledValue` in `DateCalendar` and `DateRangeCalendar` components to point to the new location in `../../utils/hooks`. - Enhanced type handling for the `selected` state to accommodate `null` values, ensuring better compatibility with the updated hook. - Adjusted the `selected` prop to default to `undefined` when `null`, improving the handling of unselected states. - Modified the `Default` story for `DateCalendar` to include example arguments for `defaultSelected` and `selected`, enhancing story clarity.
24b85da
to
b9cc963
Compare
Thank you, @chihsuan ! I'll take a look in the upcoming days :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for working on this! I suggested a few tweaks, but the overall approach is good :)
defaultSelected: new Date( 1751472000000 ), | ||
selected: new Date( 1751558400000 ), | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which errors? AFAIK it's fine for DateCalendar
not to have those props defined. Besides, it wouldn't make sense to define both defaultSelected
and selected
at the same time — one is for uncontrolled usage, the other one for controlled usage.
We should be able to undo these changes without causing TS errors
const [ selected, setSelected ] = useControlledValue< | ||
Date | undefined | null | ||
>( { | ||
defaultValue: defaultSelected, | ||
value: selectedProp, | ||
// @ts-ignore: The useControlledValue expects onChange value parameter type is the same as the value parameter, but in our case, onSelect always receives DateRange | undefined. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly to how we now handle the fallback to ?? undefined
when passing the selected
value to DayPicker
, we can write an internal onChange
callback to do the same conversion and avoid the @ts-ignore
diff --git i/packages/components/src/calendar/date-calendar/index.tsx w/packages/components/src/calendar/date-calendar/index.tsx
index a77da53060..f70807d4d3 100644
--- i/packages/components/src/calendar/date-calendar/index.tsx
+++ w/packages/components/src/calendar/date-calendar/index.tsx
@@ -3,6 +3,10 @@
*/
import { DayPicker } from 'react-day-picker';
import { enUS } from 'react-day-picker/locale';
+/**
+ * WordPress dependencies
+ */
+import { useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
@@ -10,7 +14,7 @@ import { COMMON_PROPS } from '../utils/constants';
import { clampNumberOfMonths } from '../utils/misc';
import { useControlledValue } from '../../utils/hooks';
import { useLocalizationProps } from '../utils/use-localization-props';
-import type { DateCalendarProps } from '../types';
+import type { DateCalendarProps, OnSelectHandler } from '../types';
/**
* `DateCalendar` is a React component that provides a customizable calendar
@@ -35,14 +39,21 @@ export const DateCalendar = ( {
mode: 'single',
} );
- const [ selected, setSelected ] = useControlledValue<
- Date | undefined | null
- >( {
- defaultValue: defaultSelected,
- value: selectedProp,
- // @ts-ignore: The useControlledValue expects onChange value parameter type is the same as the value parameter, but in our case, onSelect always receives DateRange | undefined.
- onChange: onSelect,
- } );
+ const onChange: OnSelectHandler< typeof selectedProp > = useCallback(
+ ( selected, triggerDate, modifiers, e ) => {
+ // Convert internal `null` to `undefined` for the public event handler.
+ onSelect?.( selected ?? undefined, triggerDate, modifiers, e );
+ },
+ [ onSelect ]
+ );
+
+ const [ selected, setSelected ] = useControlledValue< typeof selectedProp >(
+ {
+ defaultValue: defaultSelected,
+ value: selectedProp,
+ onChange,
+ }
+ );
return (
<DayPicker
diff --git i/packages/components/src/calendar/date-range-calendar/index.tsx w/packages/components/src/calendar/date-range-calendar/index.tsx
index 0d4871103b..2b40e3c0a4 100644
--- i/packages/components/src/calendar/date-range-calendar/index.tsx
+++ w/packages/components/src/calendar/date-range-calendar/index.tsx
@@ -7,7 +7,7 @@ import { enUS } from 'react-day-picker/locale';
/**
* WordPress dependencies
*/
-import { useMemo, useState } from '@wordpress/element';
+import { useMemo, useState, useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
@@ -15,7 +15,11 @@ import { COMMON_PROPS, MODIFIER_CLASSNAMES } from '../utils/constants';
import { clampNumberOfMonths } from '../utils/misc';
import { useControlledValue } from '../../utils/hooks';
import { useLocalizationProps } from '../utils/use-localization-props';
-import type { DateRangeCalendarProps, DateRange } from '../types';
+import type {
+ DateRangeCalendarProps,
+ DateRange,
+ OnSelectHandler,
+} from '../types';
export function usePreviewRange( {
selected,
@@ -151,13 +155,20 @@ export const DateRangeCalendar = ( {
mode: 'range',
} );
+ const onChange: OnSelectHandler< typeof selectedProp > = useCallback(
+ ( selected, triggerDate, modifiers, e ) => {
+ // Convert internal `null` to `undefined` for the public event handler.
+ onSelect?.( selected ?? undefined, triggerDate, modifiers, e );
+ },
+ [ onSelect ]
+ );
+
const [ selected, setSelected ] = useControlledValue<
DateRange | undefined | null
>( {
defaultValue: defaultSelected,
value: selectedProp,
- // @ts-ignore: The useControlledValue expects onChange value parameter type is the same as the value parameter, but in our case, onSelect always receives DateRange | undefined.
- onChange: onSelect,
+ onChange,
} );
const [ hoveredDate, setHoveredDate ] = useState< Date | undefined >(
diff --git i/packages/components/src/calendar/types.ts w/packages/components/src/calendar/types.ts
index dc4bb72d5b..931556aaf5 100644
--- i/packages/components/src/calendar/types.ts
+++ w/packages/components/src/calendar/types.ts
@@ -153,7 +153,7 @@ type DayOfWeek = {
* @param {Modifiers} modifiers - The modifiers associated with the event.
* @param {React.MouseEvent | React.KeyboardEvent} e - The event object.
*/
-type OnSelectHandler< T > = (
+export type OnSelectHandler< T > = (
selected: T,
triggerDate: Date,
modifiers: Modifiers,
diff --git i/packages/components/src/utils/hooks/use-controlled-value.ts w/packages/components/src/utils/hooks/use-controlled-value.ts
index b43f670332..725283736f 100644
--- i/packages/components/src/utils/hooks/use-controlled-value.ts
+++ w/packages/components/src/utils/hooks/use-controlled-value.ts
@@ -48,5 +48,5 @@ export function useControlledValue< T >( {
setValue = setState;
}
- return [ value, setValue as typeof setState ] as const;
+ return [ value, setValue ] as const;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the detailed feedback and the comprehensive diff!
I initially went with @ts-ignore
to keep the changes minimal, but after considering your approach, I agree that the explicit onChange
callback is much better for several.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated as suggested in 8db0b82
@@ -48,7 +48,9 @@ function UnforwardedToggleGroupControlAsButtonGroup( | |||
const { value, defaultValue } = | |||
useComputeControlledOrUncontrolledValue( valueProp ); | |||
|
|||
const [ selectedValue, setSelectedValue ] = useControlledValue( { | |||
const [ selectedValue, setSelectedValue ] = useControlledValue< | |||
typeof value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ciampo Thank you for the review! I've addressed all the review comments. All changes are ready for another review. 🙏 |
}, | ||
[ onSelect ] | ||
); | ||
|
||
const [ selected, setSelected ] = useControlledValue< | ||
DateRange | undefined | null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: could this be also changed to typeof selectedProp
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a further improvement, it would be nice to make the useControlledValue
hook infer
the T
type from the received arguments, but that's definitely something we don't have to work on today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: could this be also changed to typeof selectedProp ?
Abosolutely, changed it to typeof selectedProp
in 8c3977f. 🙌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🚀 We can merge once this last comment is addressed
Thank you, @ciampo! I've made the changes. Since I don't have merge permissions, could you or someone else please handle the merge? Much appreciated! 🙏 |
…onents (WordPress#70655) * Refactor `useControlledValue` hook to enhance onChange handling - Updated the `onChange` prop to accept additional arguments for better flexibility. - Introduced a new `uncontrolledSetValue` function using `useCallback` to streamline state updates and onChange calls in uncontrolled mode. - Improved code clarity by separating controlled and uncontrolled mode logic. This change enhances the usability of the hook in various scenarios. * Refactor calendar components to utilize updated `useControlledValue` hook - Updated imports of `useControlledValue` in `DateCalendar` and `DateRangeCalendar` components to point to the new location in `../../utils/hooks`. - Enhanced type handling for the `selected` state to accommodate `null` values, ensuring better compatibility with the updated hook. - Adjusted the `selected` prop to default to `undefined` when `null`, improving the handling of unselected states. - Modified the `Default` story for `DateCalendar` to include example arguments for `defaultSelected` and `selected`, enhancing story clarity. * Improve docs * Add changelog * Remove the args from DateCalendar story * Address review feedback: Replace @ts-ignore with proper onChange callbacks * Update useControlledValue type to match selectedProp
What?
This PR consolidates the
useControlledValue
hook implementation by enhancing the existinguseControlledValue
hook and removing the duplicate Calendar-specific version.Why?
cc @ciampo
Follow-up to #70578 addressing this review comment to reuse the existing
useControlledValue
hook in the package instead of creating a duplicate implementation.This change:
How?
The key differences between these two
useControlledValue
hooks are:1. Type signature:
Value:
value?: T
value?: T | null | undefined
onChange:
onChange?: (value: T) => void
onChange?: (newValue: T, ...args: any[]) => void
2. Value handling:
const value = hasValue ? valueProp : state;
const value = (hasValue ? valueProp : state) ?? undefined;
3. Implementation approach:
Overall, the calendar version is more specialized for date handling (null values) and supports additional arguments in onChange callbacks.
To reuse the existing
useControlledValue
hook in the Calendar components, I made the following changes to the shared version:onChange
callbacks (...args
).useCallback
for uncontrolled mode. This is not strictly necessary for Calendar, but it's a good practice to use it IMO. If there is a concern about introducing this change, we can revert it.And then, I updated the Calendar components:
null
values to undefined for selected value in Calendar components to ensure correct behavior.Testing Instructions
Note: Existing Calendar component tests have covered controlled/uncontrolled modes, onChange callbacks with additional arguments, and state transitions.
npm run storybook:dev
defaultSelected
andselected
props.Testing Instructions for Keyboard