From 73071ec17939366ab511e1b0a097be1ec66da818 Mon Sep 17 00:00:00 2001 From: Ddouglasz Date: Tue, 10 Jun 2025 17:02:49 +0200 Subject: [PATCH 1/7] feat(filters): add `appearance` prop to date input components for filter styling --- .changeset/polite-apes-create.md | 29 ++++++++++++++++ .../src/calendar-body/calendar-body.styles.ts | 25 +++++++++++--- .../src/calendar-body/calendar-body.tsx | 5 +++ .../src/calendar-menu/calendar-menu.tsx | 23 ++++++++++--- .../date-input/src/date-input.stories.tsx | 34 +++++++++++++++++++ .../inputs/date-input/src/date-input.tsx | 13 ++++++- .../date-input/src/date-input.visualspec.js | 5 +++ .../src/date-range-input.stories.tsx | 29 ++++++++++++++++ .../date-range-input/src/date-range-input.tsx | 14 +++++++- .../src/date-time-input.stories.tsx | 25 ++++++++++++++ .../date-time-input/src/date-time-input.tsx | 14 ++++++-- .../src/date-time-input.visualspec.js | 5 +++ 12 files changed, 209 insertions(+), 12 deletions(-) create mode 100644 .changeset/polite-apes-create.md diff --git a/.changeset/polite-apes-create.md b/.changeset/polite-apes-create.md new file mode 100644 index 0000000000..d7d6054956 --- /dev/null +++ b/.changeset/polite-apes-create.md @@ -0,0 +1,29 @@ +--- +'@commercetools-uikit/date-range-input': minor +'@commercetools-uikit/date-time-input': minor +'@commercetools-uikit/date-input': minor +'@commercetools-uikit/calendar-utils': minor +--- + +feat: add `appearance` prop with 'filter' option to date input components + +To use the date filters, there are some visual modifications that need to happen in the different date inputs to support the designs and ux of the filters pattern. Most of these changes are dependent on new props to set these options when the component is used in a filter component. + +Add support for `appearance: 'filter'` to DateInput, DateTimeInput, and DateRangeInput components. When set to 'filter', the components: + +- Remove borders and box shadows for a clean, inline appearance +- Keep the calendar always open (when not disabled or read-only) +- Maintain transparent backgrounds to blend seamlessly with filter UIs + +This follows the same design pattern established in select input components and enables date inputs to be used effectively within filter components and search interfaces. + +**New Props:** +- `appearance?: 'default' | 'filter'` - Controls the visual styling of the date input + +**Examples:** +```jsx + + + +``` +``` diff --git a/packages/calendar-utils/src/calendar-body/calendar-body.styles.ts b/packages/calendar-utils/src/calendar-body/calendar-body.styles.ts index 8442341d03..00d700be0c 100644 --- a/packages/calendar-utils/src/calendar-body/calendar-body.styles.ts +++ b/packages/calendar-utils/src/calendar-body/calendar-body.styles.ts @@ -27,10 +27,13 @@ const getClearSectionStyles = () => { }; type TState = { - isFocused: boolean; + isFocused?: boolean; }; const getIconBorderColor = (props: TCalendarBody, state: TState) => { + if (props.appearance === 'filter') { + return designTokens.colorTransparent; + } if (props.isDisabled) { return designTokens.borderColorForInputWhenDisabled; } @@ -89,13 +92,18 @@ const getCalendarIconContainerStyles = ( &:active, &:hover:not(:disabled)&:not(:read-only), &:focus { - border-color: ${designTokens.borderColorForInputWhenFocused}; + border-color: ${props.appearance === 'filter' + ? designTokens.colorTransparent + : designTokens.borderColorForInputWhenFocused}; } `, ]; }; const getInputBorderColor = (props: TCalendarBody, state: TState) => { + if (props.appearance === 'filter') { + return designTokens.colorTransparent; + } if (props.isDisabled) { return designTokens.borderColorForInputWhenDisabled; } @@ -108,7 +116,7 @@ const getInputBorderColor = (props: TCalendarBody, state: TState) => { if (props.isReadOnly) { return designTokens.borderColorForInputWhenReadonly; } - if ((props.isOpen || state.isFocused) && !props.isReadOnly) { + if (props.isOpen || state.isFocused) { return designTokens.borderColorForInputWhenFocused; } return designTokens.borderColorForInput; @@ -131,6 +139,9 @@ const getInputFontColor = (props: TCalendarBody) => { }; const getInputContainerBackgroundColor = (props: TCalendarBody) => { + if (props.appearance === 'filter') { + return designTokens.colorTransparent; + } if (props.isDisabled) { return designTokens.backgroundColorForInputWhenDisabled; } @@ -164,7 +175,9 @@ const getInputContainerStyles = (props: TCalendarBody, state: TState) => { &:hover:not(:focus) { background-color: ${!props.isDisabled && !props.isReadOnly - ? designTokens.backgroundColorForInputWhenHovered + ? props.appearance === 'filter' + ? designTokens.colorTransparent + : designTokens.backgroundColorForInputWhenHovered : null}; } &:focus { @@ -174,10 +187,13 @@ const getInputContainerStyles = (props: TCalendarBody, state: TState) => { props.isReadOnly || ((props.isOpen || state.isFocused) && !props.isReadOnly) ? '' + : props.appearance === 'filter' + ? designTokens.colorTransparent : designTokens.borderColorForInputWhenFocused}; } `, !props.isReadOnly && + props.appearance !== 'filter' && css` &:focus-within { border-color: ${designTokens.borderColorForInputWhenFocused}; @@ -189,6 +205,7 @@ const getInputContainerStyles = (props: TCalendarBody, state: TState) => { } `, (props.hasError || props.hasWarning) && + props.appearance !== 'filter' && css` box-shadow: inset 0 0 0 1px; `, diff --git a/packages/calendar-utils/src/calendar-body/calendar-body.tsx b/packages/calendar-utils/src/calendar-body/calendar-body.tsx index e76c95077d..614027dc05 100644 --- a/packages/calendar-utils/src/calendar-body/calendar-body.tsx +++ b/packages/calendar-utils/src/calendar-body/calendar-body.tsx @@ -77,6 +77,11 @@ export type TCalendarBody = { placeholder?: string; /** @deprecated */ theme?: Theme; + /** + * Indicates the appearance of the input. + * Filter appearance removes borders and box shadows for use in filter components. + */ + appearance?: 'default' | 'filter'; }; export const CalendarBody = ({ diff --git a/packages/calendar-utils/src/calendar-menu/calendar-menu.tsx b/packages/calendar-utils/src/calendar-menu/calendar-menu.tsx index 00e3dcc9bc..b7925d2873 100644 --- a/packages/calendar-utils/src/calendar-menu/calendar-menu.tsx +++ b/packages/calendar-utils/src/calendar-menu/calendar-menu.tsx @@ -8,12 +8,23 @@ type TCalendarMenu = { hasError?: boolean; hasWarning?: boolean; footer?: ReactNode; + /** + * Indicates the appearance of the calendar menu. + * Filter appearance removes box shadows and positioning for inline display. + */ + appearance?: 'default' | 'filter'; }; export default class CalendarMenu extends Component { static displayName = 'CalendarMenu'; render() { - const { hasFooter, hasWarning, hasError, ...rest } = this.props; + const { + hasFooter, + hasWarning, + hasError, + appearance = 'default', + ...rest + } = this.props; return (
{ color: ${designTokens.colorSolid}; font-family: inherit; border: none; - box-shadow: 0 2px 5px 0px rgba(0, 0, 0, 0.15); + box-shadow: ${appearance === 'filter' + ? 'none' + : '0 2px 5px 0px rgba(0, 0, 0, 0.15)'}; border-radius: ${designTokens.borderRadiusForInput}; margin-top: ${designTokens.spacing10}; font-size: ${designTokens.fontSize30}; - position: absolute; + position: ${appearance === 'filter' ? 'inherit' : 'absolute'}; box-sizing: border-box; width: 100%; background-color: ${designTokens.colorSurface}; min-width: ${designTokens.constraint5}; - z-index: 99999; /* copied from flatpickr */ + z-index: ${appearance === 'filter' + ? 'inherit' + : '99999'}; /* copied from flatpickr */ `, !hasFooter && css` diff --git a/packages/components/inputs/date-input/src/date-input.stories.tsx b/packages/components/inputs/date-input/src/date-input.stories.tsx index eb3eb9be0a..b20f027674 100644 --- a/packages/components/inputs/date-input/src/date-input.stories.tsx +++ b/packages/components/inputs/date-input/src/date-input.stories.tsx @@ -5,6 +5,12 @@ import { useEffect, useState } from 'react'; const meta: Meta = { title: 'Form/Inputs/DateInput', component: DateInput, + argTypes: { + appearance: { + control: { type: 'select' }, + options: ['default', 'filter'], + }, + }, decorators: [ (Story) => (
@@ -43,5 +49,33 @@ export const BasicExample: Story = { id: 'date-input', horizontalConstraint: 7, value: '', + appearance: 'default', + }, +}; + +export const FilterAppearance: Story = { + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [value, setValue] = useState(args.value); + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + setValue(args.value || ''); + }, [args.value]); + + return ( +
+ setValue(e.target.value || '')} + /> +
+ ); + }, + args: { + id: 'date-input-filter', + horizontalConstraint: 7, + value: '', + appearance: 'filter', }, }; diff --git a/packages/components/inputs/date-input/src/date-input.tsx b/packages/components/inputs/date-input/src/date-input.tsx index fb431efd66..2f72c96861 100644 --- a/packages/components/inputs/date-input/src/date-input.tsx +++ b/packages/components/inputs/date-input/src/date-input.tsx @@ -135,6 +135,11 @@ export type TDateInput = { * A maximum selectable date. Must either be an empty string or a date formatted as "YYYY-MM-DD". */ maxValue?: string; + /** + * Indicates the appearance of the input. + * Filter appearance removes borders and box shadows, and calendar is always open. + */ + appearance?: 'default' | 'filter'; }; const DateInput = (props: TDateInput) => { @@ -145,6 +150,7 @@ const DateInput = (props: TDateInput) => { number | null | undefined >(props.value === '' ? null : getDateInMonth(props.value) - 1); const inputRef = useRef(null); + const appearance = props.appearance || 'default'; if (!props.isReadOnly) { warning( @@ -277,6 +283,7 @@ const DateInput = (props: TDateInput) => {
{ hasError={props.hasError} hasWarning={props.hasWarning} /> - {isOpen && !props.isDisabled && !props.isReadOnly && ( + {((isOpen && !props.isDisabled && !props.isReadOnly) || + (appearance === 'filter' && + !props.isDisabled && + !props.isReadOnly)) && ( { await input.type('2017'); await queries.findByText(doc, '2017'); }); + it('Filter Appearance', async () => { + await page.goto(`${globalThis.HOST}/date-input--filter-appearance`); + await page.waitForSelector('text/November'); + await percySnapshot(page, 'DateInput - Filter Appearance'); + }); }); diff --git a/packages/components/inputs/date-range-input/src/date-range-input.stories.tsx b/packages/components/inputs/date-range-input/src/date-range-input.stories.tsx index 00f103eba1..c1c4373456 100644 --- a/packages/components/inputs/date-range-input/src/date-range-input.stories.tsx +++ b/packages/components/inputs/date-range-input/src/date-range-input.stories.tsx @@ -6,6 +6,12 @@ import { useState } from 'react'; const meta: Meta = { title: 'Form/Inputs/DateRangeInput', component: DateRangeInputProxy, + argTypes: { + appearance: { + control: { type: 'select' }, + options: ['default', 'filter'], + }, + }, }; export default meta; @@ -32,4 +38,27 @@ export const BasicExample: Story = (args: TDateRangeInputProps) => { BasicExample.args = { horizontalConstraint: 10, isClearable: true, + appearance: 'default', +}; + +export const FilterAppearance: Story = (args: TDateRangeInputProps) => { + const [value, setValue] = useState([ + '2024-11-13', + '2024-11-16', + ]); + return ( +
+ setValue(e.target.value as DateRangeArray)} + value={value} + /> +
+ ); +}; + +FilterAppearance.args = { + horizontalConstraint: 10, + isClearable: true, + appearance: 'filter', }; diff --git a/packages/components/inputs/date-range-input/src/date-range-input.tsx b/packages/components/inputs/date-range-input/src/date-range-input.tsx index b5ea71299a..1469319fc3 100644 --- a/packages/components/inputs/date-range-input/src/date-range-input.tsx +++ b/packages/components/inputs/date-range-input/src/date-range-input.tsx @@ -192,6 +192,11 @@ export type TDateRangeInputProps = { * Indicates the input field has warning */ hasWarning?: boolean; + /** + * Indicates the appearance of the input. + * Filter appearance removes borders and box shadows, and calendar is always open. + */ + appearance?: 'default' | 'filter'; } & WrappedComponentProps; type TDateRangeInputState = { @@ -285,6 +290,8 @@ class DateRangeInput extends Component< }); }; render() { + const appearance = this.props.appearance || 'default'; + return ( - {isOpen && !this.props.isDisabled && ( + {((isOpen && !this.props.isDisabled) || + (appearance === 'filter' && + !this.props.isDisabled && + !this.props.isReadOnly)) && ( = { 'Europe/Amsterdam', ], }, + appearance: { + control: { type: 'select' }, + options: ['default', 'filter'], + }, }, }; export default meta; @@ -40,4 +44,25 @@ export const BasicExample: Story = (args: TDateTimeInputProps) => { BasicExample.args = { timeZone: 'UTC', horizontalConstraint: 8, + appearance: 'default', +}; + +export const FilterAppearance: Story = (args: TDateTimeInputProps) => { + const [value, setValue] = useState(''); + + return ( +
+ setValue(e.target.value || '')} + value={value} + /> +
+ ); +}; + +FilterAppearance.args = { + timeZone: 'UTC', + horizontalConstraint: 8, + appearance: 'filter', }; diff --git a/packages/components/inputs/date-time-input/src/date-time-input.tsx b/packages/components/inputs/date-time-input/src/date-time-input.tsx index d7f1eb5741..c3e1854021 100644 --- a/packages/components/inputs/date-time-input/src/date-time-input.tsx +++ b/packages/components/inputs/date-time-input/src/date-time-input.tsx @@ -180,9 +180,14 @@ export type TDateTimeInputProps = { hasWarning?: boolean; /** * The time that will be used by default when a user selects a calendar day. - * It must follow the “HH:mm” pattern (eg: 04:30, 13:25, 23:59) + * It must follow the "HH:mm" pattern (eg: 04:30, 13:25, 23:59) */ defaultDaySelectionTime?: string; + /** + * Indicates the appearance of the input. + * Filter appearance removes borders and box shadows, and calendar is always open. + */ + appearance?: 'default' | 'filter'; } & WrappedComponentProps; type TDateTimeInputState = { @@ -281,6 +286,8 @@ class DateTimeInput extends Component< ); } + const appearance = this.props.appearance || 'default'; + return ( - {isOpen && !this.props.isDisabled && ( + {((isOpen && !this.props.isDisabled) || + (appearance === 'filter' && !this.props.isDisabled)) && ( { // TODO: uncomment when issue with Percy is resolved // await percySnapshot(page, 'DateTimeInput - open'); }); + it('Filter Appearance', async () => { + await page.goto(`${globalThis.HOST}/date-time-input--filter-appearance`); + await page.waitForSelector('text/November'); + await percySnapshot(page, 'DateTimeInput - Filter Appearance'); + }); }); From 998730d99f02a0b601965cdf483d19acb2fee39f Mon Sep 17 00:00:00 2001 From: Ddouglasz Date: Tue, 10 Jun 2025 17:16:48 +0200 Subject: [PATCH 2/7] refactor(calendar): remove unused filter appearance handling in input border color logic --- .../calendar-utils/src/calendar-body/calendar-body.styles.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/calendar-utils/src/calendar-body/calendar-body.styles.ts b/packages/calendar-utils/src/calendar-body/calendar-body.styles.ts index 00d700be0c..7e05002a94 100644 --- a/packages/calendar-utils/src/calendar-body/calendar-body.styles.ts +++ b/packages/calendar-utils/src/calendar-body/calendar-body.styles.ts @@ -101,9 +101,6 @@ const getCalendarIconContainerStyles = ( }; const getInputBorderColor = (props: TCalendarBody, state: TState) => { - if (props.appearance === 'filter') { - return designTokens.colorTransparent; - } if (props.isDisabled) { return designTokens.borderColorForInputWhenDisabled; } From 3a20e44044a9b97d85970defc01087f8a0e74067 Mon Sep 17 00:00:00 2001 From: Ddouglasz Date: Wed, 11 Jun 2025 11:29:43 +0200 Subject: [PATCH 3/7] feat(date-input): add filter appearance spec for DateInput and DateTimeInput components --- .../inputs/date-input/src/date-input.visualroute.jsx | 10 ++++++++++ .../src/date-time-input.visualroute.jsx | 10 ++++++++++ .../date-time-input/src/date-time-input.visualspec.js | 5 ----- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/components/inputs/date-input/src/date-input.visualroute.jsx b/packages/components/inputs/date-input/src/date-input.visualroute.jsx index d9e4fd4b00..d8c7a1ba49 100644 --- a/packages/components/inputs/date-input/src/date-input.visualroute.jsx +++ b/packages/components/inputs/date-input/src/date-input.visualroute.jsx @@ -122,5 +122,15 @@ export const component = () => ( placeholder="Select something" /> + + {}} + isCondensed={true} + horizontalConstraint={7} + placeholder="Select something" + appearance="filter" + /> + ); diff --git a/packages/components/inputs/date-time-input/src/date-time-input.visualroute.jsx b/packages/components/inputs/date-time-input/src/date-time-input.visualroute.jsx index 7f78f203e4..b9cc673ce2 100644 --- a/packages/components/inputs/date-time-input/src/date-time-input.visualroute.jsx +++ b/packages/components/inputs/date-time-input/src/date-time-input.visualroute.jsx @@ -140,5 +140,15 @@ export const component = () => ( horizontalConstraint={7} /> + + {}} + horizontalConstraint={7} + appearance="filter" + /> + ); diff --git a/packages/components/inputs/date-time-input/src/date-time-input.visualspec.js b/packages/components/inputs/date-time-input/src/date-time-input.visualspec.js index 02766133c3..54c576bb82 100644 --- a/packages/components/inputs/date-time-input/src/date-time-input.visualspec.js +++ b/packages/components/inputs/date-time-input/src/date-time-input.visualspec.js @@ -13,9 +13,4 @@ describe('DateTimeInput', () => { // TODO: uncomment when issue with Percy is resolved // await percySnapshot(page, 'DateTimeInput - open'); }); - it('Filter Appearance', async () => { - await page.goto(`${globalThis.HOST}/date-time-input--filter-appearance`); - await page.waitForSelector('text/November'); - await percySnapshot(page, 'DateTimeInput - Filter Appearance'); - }); }); From 3df68445697fc80ceff808b9ee168c878da51807 Mon Sep 17 00:00:00 2001 From: Ddouglasz Date: Wed, 11 Jun 2025 12:23:14 +0200 Subject: [PATCH 4/7] chore(date-input): remove filter appearance test from DateInput visual specs --- .../inputs/date-input/src/date-input.visualspec.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/components/inputs/date-input/src/date-input.visualspec.js b/packages/components/inputs/date-input/src/date-input.visualspec.js index b9c3f92f28..7080b1172e 100644 --- a/packages/components/inputs/date-input/src/date-input.visualspec.js +++ b/packages/components/inputs/date-input/src/date-input.visualspec.js @@ -33,9 +33,4 @@ describe('DateInput', () => { await input.type('2017'); await queries.findByText(doc, '2017'); }); - it('Filter Appearance', async () => { - await page.goto(`${globalThis.HOST}/date-input--filter-appearance`); - await page.waitForSelector('text/November'); - await percySnapshot(page, 'DateInput - Filter Appearance'); - }); }); From 18812b3d6b5b36588882506769d06dd6b1f22666 Mon Sep 17 00:00:00 2001 From: Ddouglasz Date: Wed, 11 Jun 2025 15:38:17 +0200 Subject: [PATCH 5/7] feat(filters): add date filtering with operators and UI components --- .../src/calendar-body/calendar-body.tsx | 73 +++-- .../filters/src/filters.stories.tsx | 308 ++++++++++++++++++ .../filters/src/fixtures/constants.tsx | 13 + .../filters/src/fixtures/inputs.tsx | 114 +++++++ 4 files changed, 474 insertions(+), 34 deletions(-) diff --git a/packages/calendar-utils/src/calendar-body/calendar-body.tsx b/packages/calendar-utils/src/calendar-body/calendar-body.tsx index 614027dc05..6ea7c4a3aa 100644 --- a/packages/calendar-utils/src/calendar-body/calendar-body.tsx +++ b/packages/calendar-utils/src/calendar-body/calendar-body.tsx @@ -156,42 +156,47 @@ export const CalendarBody = ({ onBlur={handleInputBlur} aria-readonly={props.isReadOnly} /> - {!disabledOrReadOnly && props.hasSelection && isClearable && ( - - )} - + {props.appearance !== 'filter' && ( + + )}
); diff --git a/packages/components/filters/src/filters.stories.tsx b/packages/components/filters/src/filters.stories.tsx index 56a27d6c3d..1d3146dd77 100644 --- a/packages/components/filters/src/filters.stories.tsx +++ b/packages/components/filters/src/filters.stories.tsx @@ -11,6 +11,10 @@ import { PrimaryColorsRadioInput, PrimaryColorsTextInput, OperatorsInput, + DateRangeFilterInput, + DateFilterWithOperator, + DateTimeFilterWithOperator, + DateOperatorsInput, } from './fixtures/inputs'; import { FILTER_GROUP_KEYS, @@ -347,3 +351,307 @@ export const BasicExample: Story = (props: TFiltersPropsWithCustomArgs) => { /> ); }; + +export const DateFiltersExample: Story = () => { + // simulate state from parent application for each date filter + // Pending values (before apply) + const [pendingDateValue, setPendingDateValue] = useState( + '' + ); + const [pendingDateTimeValue, setPendingDateTimeValue] = useState< + string | string[] + >(''); + const [pendingDateRangeValue, setPendingDateRangeValue] = useState( + [] + ); + + // Applied values (after clicking apply) + const [appliedDateValue, setAppliedDateValue] = useState< + TFiltersProps['appliedFilters'] + >([]); + const [appliedDateTimeValue, setAppliedDateTimeValue] = useState< + TFiltersProps['appliedFilters'] + >([]); + const [appliedDateRangeValue, setAppliedDateRangeValue] = useState< + TFiltersProps['appliedFilters'] + >([]); + + // Operators for each date filter + const [dateOperator, setDateOperator] = useState('is'); + const [dateTimeOperator, setDateTimeOperator] = useState('is'); + const [dateRangeOperator, setDateRangeOperator] = + useState('is between'); + + // Helper function to get applied values for a filter + const getDateAppliedValue = (): TFiltersProps['appliedFilters'] => { + if ( + dateOperator === 'is between' && + Array.isArray(pendingDateValue) && + pendingDateValue.length === 2 + ) { + return [ + { + filterKey: 'date', + values: [ + { + value: pendingDateValue.join(' - '), + label: `${pendingDateValue[0]} - ${pendingDateValue[1]}`, + }, + ], + }, + ]; + } else if (pendingDateValue && !Array.isArray(pendingDateValue)) { + return [ + { + filterKey: 'date', + values: [ + { + value: pendingDateValue, + label: pendingDateValue, + }, + ], + }, + ]; + } + return []; + }; + + const getDateTimeAppliedValue = (): TFiltersProps['appliedFilters'] => { + if ( + dateTimeOperator === 'is between' && + Array.isArray(pendingDateTimeValue) && + pendingDateTimeValue.length === 2 + ) { + return [ + { + filterKey: 'dateTime', + values: [ + { + value: pendingDateTimeValue.join(' - '), + label: `${pendingDateTimeValue[0]} - ${pendingDateTimeValue[1]}`, + }, + ], + }, + ]; + } else if (pendingDateTimeValue && !Array.isArray(pendingDateTimeValue)) { + return [ + { + filterKey: 'dateTime', + values: [ + { + value: pendingDateTimeValue, + label: new Date(pendingDateTimeValue).toLocaleString(), + }, + ], + }, + ]; + } + return []; + }; + + const getDateRangeAppliedValue = (): TFiltersProps['appliedFilters'] => { + if (pendingDateRangeValue.length === 2) { + return [ + { + filterKey: 'dateRange', + values: [ + { + value: pendingDateRangeValue.join(' - '), + label: `${pendingDateRangeValue[0]} - ${pendingDateRangeValue[1]}`, + }, + ], + }, + ]; + } + return []; + }; + + // Clear functions for pending values + const clearDateFilter = () => { + setPendingDateValue(dateOperator === 'is between' ? [] : ''); + setAppliedDateValue([]); + }; + + const clearDateTimeFilter = () => { + setPendingDateTimeValue(dateTimeOperator === 'is between' ? [] : ''); + setAppliedDateTimeValue([]); + }; + + const clearDateRangeFilter = () => { + setPendingDateRangeValue([]); + setAppliedDateRangeValue([]); + }; + + // Clear all filters + const clearAllFilters = () => { + clearDateFilter(); + clearDateTimeFilter(); + clearDateRangeFilter(); + }; + + // Handle operator changes and reset values + const handleDateOperatorChange = (newOperator: string) => { + setDateOperator(newOperator); + setPendingDateValue(newOperator === 'is between' ? [] : ''); + setAppliedDateValue([]); + }; + + const handleDateTimeOperatorChange = (newOperator: string) => { + setDateTimeOperator(newOperator); + setPendingDateTimeValue(newOperator === 'is between' ? [] : ''); + setAppliedDateTimeValue([]); + }; + + // Check if apply button should be enabled + const isDateApplyEnabled = () => { + if (dateOperator === 'is between') { + return Array.isArray(pendingDateValue) && pendingDateValue.length === 2; + } + return pendingDateValue && !Array.isArray(pendingDateValue); + }; + + const isDateTimeApplyEnabled = () => { + if (dateTimeOperator === 'is between') { + return ( + Array.isArray(pendingDateTimeValue) && pendingDateTimeValue.length === 2 + ); + } + return pendingDateTimeValue && !Array.isArray(pendingDateTimeValue); + }; + + const isDateRangeApplyEnabled = () => { + return pendingDateRangeValue.length === 2; + }; + + // generate 'appliedFilters' state based on applied values + const appliedFilters: TFiltersProps['appliedFilters'] = [ + ...appliedDateValue, + ...appliedDateTimeValue, + ...appliedDateRangeValue, + ]; + + const filters = [ + { + key: 'date', + label: 'Date', + operatorLabel: dateOperator, + groupKey: FILTER_GROUP_KEYS.dateFilters, + filterMenuConfiguration: { + renderMenuBody: () => ( + + ), + renderOperatorsInput: () => ( + + ), + renderApplyButton: () => ( + { + setAppliedDateValue(getDateAppliedValue()); + }} + isDisabled={!isDateApplyEnabled()} + label="Apply" + size="10" + /> + ), + onClearRequest: clearDateFilter, + }, + }, + { + key: 'dateTime', + label: 'Date & Time', + operatorLabel: dateTimeOperator, + groupKey: FILTER_GROUP_KEYS.dateFilters, + filterMenuConfiguration: { + renderMenuBody: () => ( + + ), + renderOperatorsInput: () => ( + + ), + renderApplyButton: () => ( + { + setAppliedDateTimeValue(getDateTimeAppliedValue()); + }} + isDisabled={!isDateTimeApplyEnabled()} + label="Apply" + size="10" + /> + ), + onClearRequest: clearDateTimeFilter, + }, + }, + { + key: 'dateRange', + label: 'Date Range', + operatorLabel: dateRangeOperator, + groupKey: FILTER_GROUP_KEYS.dateFilters, + filterMenuConfiguration: { + renderMenuBody: () => ( + + ), + renderOperatorsInput: () => ( + + ), + renderApplyButton: () => ( + { + setAppliedDateRangeValue(getDateRangeAppliedValue()); + }} + isDisabled={!isDateRangeApplyEnabled()} + label="Apply" + size="10" + /> + ), + onClearRequest: clearDateRangeFilter, + }, + }, + ]; + + return ( +
+

+ Advanced Date Filters with Operators +

+

+ This example demonstrates advanced date filter functionality including: +
+ • Configurable operators (is, is not, is between, is before, is after) +
+ • Dynamic component switching (DateInput ↔ DateRangeInput based on + operator) +
+ • Apply button requirement for date filters +
• Clean filter appearance without calendar icons and clear buttons +

+ } + filters={filters} + filterGroups={FILTER_GROUPS} + appliedFilters={appliedFilters} + onClearAllRequest={clearAllFilters} + defaultOpen={true} + /> +
+ ); +}; diff --git a/packages/components/filters/src/fixtures/constants.tsx b/packages/components/filters/src/fixtures/constants.tsx index 9c396d264e..7e419f2f3a 100644 --- a/packages/components/filters/src/fixtures/constants.tsx +++ b/packages/components/filters/src/fixtures/constants.tsx @@ -1,6 +1,7 @@ export const FILTER_GROUP_KEYS = { primaryColors: 'primaryColors', secondaryColors: 'secondaryColors', + dateFilters: 'dateFilters', }; export const FILTER_GROUPS = [ @@ -9,6 +10,10 @@ export const FILTER_GROUPS = [ key: FILTER_GROUP_KEYS.secondaryColors, label:
Secondary Colors
, }, + { + key: FILTER_GROUP_KEYS.dateFilters, + label:
Date Filters
, + }, ]; export const PRIMARY_COLOR_OPTIONS = [ @@ -47,3 +52,11 @@ export const OPERATOR_OPTIONS = [ { value: 'is', label: 'is' }, { value: 'is not', label: 'is not' }, ]; + +export const DATE_OPERATOR_OPTIONS = [ + { value: 'is', label: 'is' }, + { value: 'is not', label: 'is not' }, + { value: 'is between', label: 'is between' }, + { value: 'is before', label: 'is before' }, + { value: 'is after', label: 'is after' }, +]; diff --git a/packages/components/filters/src/fixtures/inputs.tsx b/packages/components/filters/src/fixtures/inputs.tsx index 13cfc7c6a2..4ace27eeea 100644 --- a/packages/components/filters/src/fixtures/inputs.tsx +++ b/packages/components/filters/src/fixtures/inputs.tsx @@ -4,11 +4,15 @@ import RadioInput from '@commercetools-uikit/radio-input'; import SearchTextInput from '@commercetools-uikit/search-text-input'; import SelectInput from '@commercetools-uikit/select-input'; import TextInput from '@commercetools-uikit/text-input'; +import DateInput from '@commercetools-uikit/date-input'; +import DateTimeInput from '@commercetools-uikit/date-time-input'; +import DateRangeInput from '@commercetools-uikit/date-range-input'; import { PRIMARY_COLOR_OPTIONS, SECONDARY_COLOR_OPTIONS, FRUIT_OPTIONS, OPERATOR_OPTIONS, + DATE_OPERATOR_OPTIONS, } from './constants'; type TFiltersSelectExampleProps = { @@ -21,6 +25,17 @@ type TFiltersInputExampleProps = { onChange: Function; }; +type TDateFilterWithOperatorProps = { + value: string | string[]; + onChange: Function; + operator: string; +}; + +type TDateRangeFilterProps = { + value: string[]; + onChange: Function; +}; + export const PrimaryColorsInput = ({ value, onChange, @@ -154,3 +169,102 @@ export const OperatorsInput = ({ }} /> ); + +export const DateRangeFilterInput = ({ + value, + onChange, +}: TDateRangeFilterProps) => ( + onChange(e.target.value)} + appearance="filter" + placeholder="Select date range" + isClearable={false} + /> +); + +export const DateFilterWithOperator = ({ + value, + onChange, + operator, +}: TDateFilterWithOperatorProps) => { + // Use DateRangeInput for "is between" operator, DateInput for others + if (operator === 'is between') { + return ( + onChange(e.target.value)} + appearance="filter" + placeholder="Select date range" + isClearable={false} + /> + ); + } + + return ( + onChange(e.target.value)} + appearance="filter" + placeholder="Select date" + /> + ); +}; + +export const DateTimeFilterWithOperator = ({ + value, + onChange, + operator, +}: TDateFilterWithOperatorProps) => { + // Use DateRangeInput for "is between" operator, DateTimeInput for others + if (operator === 'is between') { + return ( + onChange(e.target.value)} + appearance="filter" + placeholder="Select date range" + isClearable={false} + /> + ); + } + + return ( + onChange(e.target.value)} + appearance="filter" + timeZone="UTC" + placeholder="Select date and time" + /> + ); +}; + +export const DateOperatorsInput = ({ + value, + onChange, +}: TFiltersInputExampleProps) => ( + { + onChange(event.target.value as string); + }} + /> +); From 3b940bd3b0bf3fd0a7505e3405110e2affe34d1a Mon Sep 17 00:00:00 2001 From: Ddouglasz Date: Wed, 11 Jun 2025 15:57:40 +0200 Subject: [PATCH 6/7] fix(filters): simplify date filter example description and remove unnecessary details --- .changeset/polite-apes-create.md | 1 - packages/components/filters/src/filters.stories.tsx | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.changeset/polite-apes-create.md b/.changeset/polite-apes-create.md index d7d6054956..942f6b90c0 100644 --- a/.changeset/polite-apes-create.md +++ b/.changeset/polite-apes-create.md @@ -26,4 +26,3 @@ This follows the same design pattern established in select input components and ``` -``` diff --git a/packages/components/filters/src/filters.stories.tsx b/packages/components/filters/src/filters.stories.tsx index 1d3146dd77..a9c3846ad8 100644 --- a/packages/components/filters/src/filters.stories.tsx +++ b/packages/components/filters/src/filters.stories.tsx @@ -634,15 +634,13 @@ export const DateFiltersExample: Story = () => { Advanced Date Filters with Operators

- This example demonstrates advanced date filter functionality including: + This example demonstrates date filter functionality including:
• Configurable operators (is, is not, is between, is before, is after)
• Dynamic component switching (DateInput ↔ DateRangeInput based on operator)
- • Apply button requirement for date filters -
• Clean filter appearance without calendar icons and clear buttons

} From ddd8ad79430488a8e481f055a755641798ea7e88 Mon Sep 17 00:00:00 2001 From: Ddouglasz Date: Thu, 12 Jun 2025 11:09:59 +0200 Subject: [PATCH 7/7] fix(filters): update styling for date filter example and adjust filter menu width --- .../components/filters/src/filter-menu/filter-menu.tsx | 2 +- packages/components/filters/src/filters.stories.tsx | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/components/filters/src/filter-menu/filter-menu.tsx b/packages/components/filters/src/filter-menu/filter-menu.tsx index 10700082f3..b4601043b4 100644 --- a/packages/components/filters/src/filter-menu/filter-menu.tsx +++ b/packages/components/filters/src/filter-menu/filter-menu.tsx @@ -100,7 +100,7 @@ export const menuStyles = css` flex-direction: column; align-items: flex-start; gap: ${designTokens.spacing30}; - width: ${designTokens.constraint6}; + width: ${designTokens.constraint7}; max-height: ${designTokens.constraint10}; padding: ${designTokens.spacing20} ${designTokens.spacing30}; background-color: ${designTokens.colorSurface}; diff --git a/packages/components/filters/src/filters.stories.tsx b/packages/components/filters/src/filters.stories.tsx index a9c3846ad8..0a6bae70df 100644 --- a/packages/components/filters/src/filters.stories.tsx +++ b/packages/components/filters/src/filters.stories.tsx @@ -633,7 +633,13 @@ export const DateFiltersExample: Story = () => {

Advanced Date Filters with Operators

-

+

This example demonstrates date filter functionality including:
• Configurable operators (is, is not, is between, is before, is after)