diff --git a/app-typescript/components/DatePicker.tsx b/app-typescript/components/DatePicker.tsx index 114d0097..b9c6562b 100644 --- a/app-typescript/components/DatePicker.tsx +++ b/app-typescript/components/DatePicker.tsx @@ -12,6 +12,8 @@ import {getWeekStartByLocale} from 'weekstart'; import {getMonthNames, getWeekdayNames} from '@sourcefabric/common'; import {localization} from '../localization'; import {assertNever} from '../helpers'; +import {TreeSelect} from './TreeSelect/TreeSelect'; +import {Icon} from './Icon'; interface IDatePickerBase extends IInputWrapper { dateFormat: string; // a combination of YYYY, MM, and DD with a custom separator e.g. 'MM/DD/YYYY' @@ -23,6 +25,9 @@ interface IDatePickerBase extends IInputWrapper { locale?: {type: 'code-only'; code: string} | {type: 'full'; payload: Omit}; hideClearButton?: boolean; + + minYearRange?: string; + maxYearRange?: string; } interface IDatePicker extends IDatePickerBase { @@ -279,6 +284,15 @@ export class DatePicker extends React.PureComponent { this.setState({valid: true, value: parseToPrimeReactCalendarFormat(this.props.value)}); } }} + monthNavigator={true} + monthNavigatorComponent={MonthNavigator} + yearNavigator={true} + yearNavigatorComponent={YearNavigator} + yearRange={ + this.props.minYearRange && this.props.maxYearRange + ? `${this.props.minYearRange}:${this.props.maxYearRange}` + : `1900:${new Date().getFullYear() + 10}` + } /> ); @@ -315,7 +329,67 @@ export class DatePickerISO extends React.PureComponent { label={this.props.label} info={this.props.info} error={this.props.error} + minYearRange={this.props.minYearRange} + maxYearRange={this.props.maxYearRange} /> ); } } + +const MonthNavigator = ({ + value, + options, + onChange, +}: React.ComponentProps>) => ( + + kind="synchronous" + value={[value]} + getOptions={() => options.map((option) => ({value: option.id}))} + getLabel={(option) => options[parseInt(option, 10)].label} + getId={(option) => option} + onChange={(selected) => { + onChange(selected[0]); + }} + clearable={false} + labelHidden + valueTemplate={(item) => ( +
+
{options[parseInt(item, 10)].label}
+ +
+ )} + inputWrapper={{ + kind: 'custom', + component: ({input}) =>
{input}
, + }} + /> +); + +const YearNavigator = ({ + value, + options, + onChange, +}: React.ComponentProps>) => ( + + kind="synchronous" + value={[value]} + getOptions={() => options.map((option) => ({value: option.id}))} + getLabel={(option) => option} + getId={(option) => option} + onChange={(selected) => { + onChange(selected[0]); + }} + clearable={false} + labelHidden + valueTemplate={(item) => ( +
+
{item}
+ +
+ )} + inputWrapper={{ + kind: 'custom', + component: ({input}) =>
{input}
, + }} + /> +); diff --git a/app-typescript/components/TreeSelect/TreeSelect.tsx b/app-typescript/components/TreeSelect/TreeSelect.tsx index b86387a3..2ce16be0 100644 --- a/app-typescript/components/TreeSelect/TreeSelect.tsx +++ b/app-typescript/components/TreeSelect/TreeSelect.tsx @@ -60,6 +60,7 @@ interface IPropsBase extends IInputWrapper { Wrapper: React.ComponentType<{backgroundColor?: string}>, ): React.ComponentType | JSX.Element; onChange(e: Array): void; + clearable?: boolean; } interface IPropsSync extends IPropsBase { @@ -258,10 +259,10 @@ export class TreeSelect extends React.Component, IState> { }); } - this.inputRef.current?.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.key === 'ArrowDown') { - e.preventDefault(); - e.stopPropagation(); + this.inputRef.current?.addEventListener('keydown', (event: KeyboardEvent) => { + if (event.key === 'ArrowDown') { + event.preventDefault(); + event.stopPropagation(); if (this.categoryButtonRef.current) { this.buttonFocus(); @@ -273,6 +274,18 @@ export class TreeSelect extends React.Component, IState> { } }); + /** + * When TreeSelect is rendered inside a popover, a click event emitted by selecting an item + * is detected by the popover, is considered as a click outside the popover and the popover is closed. + * The problem lies in the popover code where outside click is detected, + * but since TreeSelect renders options element into the body + * the element is not a descendant of the popover and it's tricky to detect the outside click. + * Thus we stop the event as a workaround. + */ + this.dropdownRef.current?.addEventListener('mousedown', (event: MouseEvent) => { + event.stopPropagation(); + }); + if (this.inputRef.current) { this.inputFocus(); } else { @@ -864,11 +877,16 @@ export class TreeSelect extends React.Component, IState> { {children} - {this.props.readOnly !== true && this.props.required !== true && ( - - - - )} + {this.props.readOnly !== true && + this.props.required !== true && + this.props.clearable !== false && ( + + + + )} ); diff --git a/app/styles/primereact/_pr-datepicker.scss b/app/styles/primereact/_pr-datepicker.scss index d29e013f..9a494fa5 100644 --- a/app/styles/primereact/_pr-datepicker.scss +++ b/app/styles/primereact/_pr-datepicker.scss @@ -9,6 +9,47 @@ line-height: 1; margin: 0 auto; text-align: center; + display: flex; + flex-direction: column; + align-items: center; + + .sd-datepicker__navigator-wrapper { + .tags-input, + .tags-input__tags { + border: none; + background: none; + padding: 0; + height: auto; + min-height: auto; + + &:after { + display: none; + } + } + + transition: + all 0.2s ease-out, + color 0.1s ease-out; + } + + .sd-datepicker__navigator-value { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + gap: $sd-base-increment * 0.5; + font-size: 1.3rem; + + &.sd-datepicker__navigator-month { + text-transform: uppercase; + font-weight: bold; + } + + &.sd-datepicker__navigator-year { + font-weight: 400; + color: var(--color-text-light); + } + } } .p-datepicker-header { @@ -70,19 +111,6 @@ } } -// display month and year on separate lines -.p-datepicker-month, -.p-datepicker-year { - display: block; - text-align: center; -} - -.p-datepicker-year { - font-weight: 400; - margin-block-start: 4px; - color: var(--color-text-light); -} - .p-datepicker { td, th { border: 0; diff --git a/package-lock.json b/package-lock.json index 6a137597..eacd5966 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "superdesk-ui-framework", - "version": "4.1.1", + "version": "4.1.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -159,9 +159,9 @@ } }, "@superdesk/primereact": { - "version": "5.0.2-13", - "resolved": "https://registry.npmjs.org/@superdesk/primereact/-/primereact-5.0.2-13.tgz", - "integrity": "sha512-v/Vzwo20voLTH+3pqhRepe3ZeOurrgOZ1wA+g9WtjMCmQGUD0eIJLS7p7bxYM60zVWHB/JWNqpPiVpf7hVck5Q==", + "version": "5.0.2-16", + "resolved": "https://registry.npmjs.org/@superdesk/primereact/-/primereact-5.0.2-16.tgz", + "integrity": "sha512-abKaxIPphR9Y7y6Mq4iPjV/ocvl7EoGPLfPpMvDs3cu8RvPeWREsaJptjuDOarTiggsYz88Mo+8CoEjqYhWKnQ==", "requires": { "react-transition-group": "^4.4.1" } diff --git a/package.json b/package.json index cc56f357..d7af3d4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "superdesk-ui-framework", - "version": "4.1.1", + "version": "4.1.2", "license": "AGPL-3.0", "repository": { "type": "git", @@ -94,7 +94,7 @@ "dependencies": { "@popperjs/core": "^2.4.0", "@sourcefabric/common": "0.0.66", - "@superdesk/primereact": "^5.0.2-13", + "@superdesk/primereact": "^5.0.2-16", "@superdesk/react-resizable-panels": "0.0.39", "chart.js": "^2.9.3", "date-fns": "^4.1.0",