Skip to content

Commit

Permalink
feat(DatePicker): add quarter precision (ant-design#6664)
Browse files Browse the repository at this point in the history
* feat(DatePicker): add quarter precision

* test(DatePicker): add test case for quarter precision

* chore: upgrade pnpm/action-setup to v4 to prevent ci failures

---------

Co-authored-by: Ke Qingrong <[email protected]>
  • Loading branch information
keqingrong and Ke Qingrong authored Jul 8, 2024
1 parent 0313082 commit dcbe575
Show file tree
Hide file tree
Showing 14 changed files with 247 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:

- name: Install pnpm
id: pnpm-install
uses: pnpm/action-setup@v2.2.4
uses: pnpm/action-setup@v4
with:
version: 7
run_install: false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/compressed-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:
- name: Install pnpm
id: pnpm-install
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 7
run_install: false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/doc-site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:

- name: Install pnpm
id: pnpm-install
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 7
run_install: false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/preview-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:

- name: Install pnpm
id: pnpm-install
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 7
run_install: false
Expand Down
20 changes: 20 additions & 0 deletions src/components/date-picker-view/demos/demo1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ export default () => {
/>
</DemoBlock>

<DemoBlock title='季度选择器' padding='0'>
<DatePickerView
onChange={val => console.log('onChange', val)}
precision='quarter'
defaultValue={now}
renderLabel={quarterLabelRenderer}
/>
</DemoBlock>

<DemoBlock title='过滤可供选择的时间' padding='0'>
<DatePickerView
defaultValue={now}
Expand Down Expand Up @@ -96,6 +105,17 @@ const weekdayLabelRenderer = (type: string, data: number) => {
}
}

const quarterLabelRenderer = (type: string, data: number) => {
switch (type) {
case 'year':
return data + '年'
case 'quarter':
return data + '季度'
default:
return data
}
}

const dateFilter: DatePickerFilter = {
day: (val, { date }) => {
// 去除所有周末
Expand Down
20 changes: 20 additions & 0 deletions src/components/date-picker-view/demos/demo2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export default () => {
filter={weekFilter}
/>
</DemoBlock>

<DemoBlock title='季度选择器过滤' padding='0'>
<DatePickerView
onChange={val => console.log('onChange', val)}
precision='quarter'
defaultValue={now}
filter={quarterFilter}
/>
</DemoBlock>
</>
)
}
Expand Down Expand Up @@ -70,3 +79,14 @@ const weekFilter: DatePickerFilter = {
return true
},
}

const quarterFilter: DatePickerFilter = {
year: val => val > new Date().getFullYear() - 3,
quarter: val => {
// 去除每年的1季度和3季度
if (val === 1 || val === 3) {
return false
}
return true
},
}
111 changes: 111 additions & 0 deletions src/components/date-picker/date-picker-quarter-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import type { ReactNode } from 'react'
import { PickerColumn } from '../picker'
import type { DatePickerFilter } from './date-picker-utils'

dayjs.extend(quarterOfYear)

export type QuarterPrecision = 'year' | 'quarter'

const precisionRankRecord: Record<QuarterPrecision, number> = {
year: 0,
quarter: 1,
}

export function generateDatePickerColumns(
selected: string[],
min: Date,
max: Date,
precision: QuarterPrecision,
renderLabel: (type: QuarterPrecision, data: number) => ReactNode,
filter: DatePickerFilter | undefined
) {
const ret: PickerColumn[] = []

const minYear = min.getFullYear()
const maxYear = max.getFullYear()

const rank = precisionRankRecord[precision]

const selectedYear = parseInt(selected[0])
const isInMinYear = selectedYear === minYear
const isInMaxYear = selectedYear === maxYear

const minDay = dayjs(min)
const maxDay = dayjs(max)
const minQuarter = minDay.quarter()
const maxQuarter = maxDay.quarter()

const generateColumn = (
from: number,
to: number,
precision: QuarterPrecision
) => {
let column: number[] = []
for (let i = from; i <= to; i++) {
column.push(i)
}
const prefix = selected.slice(0, precisionRankRecord[precision])
const currentFilter = filter?.[precision]
if (currentFilter && typeof currentFilter === 'function') {
column = column.filter(i =>
currentFilter(i, {
get date() {
const stringArray = [...prefix, i.toString()]
return convertStringArrayToDate(stringArray)
},
})
)
}
return column
}

if (rank >= precisionRankRecord.year) {
const lower = minYear
const upper = maxYear
const years = generateColumn(lower, upper, 'year')
ret.push(
years.map(v => ({
label: renderLabel('year', v),
value: v.toString(),
}))
)
}

if (rank >= precisionRankRecord.quarter) {
const lower = isInMinYear ? minQuarter : 1
const upper = isInMaxYear ? maxQuarter : 4
const quarters = generateColumn(lower, upper, 'quarter')
ret.push(
quarters.map(v => ({
label: renderLabel('quarter', v),
value: v.toString(),
}))
)
}

return ret
}

export function convertDateToStringArray(
date: Date | undefined | null
): string[] {
if (!date) return []
const day = dayjs(date)
return [day.year().toString(), day.quarter().toString()]
}

export function convertStringArrayToDate<
T extends string | number | null | undefined,
>(value: T[]): Date {
const yearString = value[0] ?? '1900'
const quarterString = value[1] ?? '1'
const day = dayjs()
.year(parseInt(yearString as string))
.quarter(parseInt(quarterString as string))
.hour(0)
.minute(0)
.second(0)
return day.toDate()
}
25 changes: 20 additions & 5 deletions src/components/date-picker/date-picker-utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { RenderLabel } from '../date-picker-view/date-picker-view'
import type { DatePrecision } from './date-picker-date-utils'
import type { WeekPrecision } from './date-picker-week-utils'
import * as dateUtils from './date-picker-date-utils'
import type { QuarterPrecision } from './date-picker-quarter-utils'
import * as quarterUtils from './date-picker-quarter-utils'
import type { WeekPrecision } from './date-picker-week-utils'
import * as weekUtils from './date-picker-week-utils'
import { RenderLabel } from '../date-picker-view/date-picker-view'
import { TILL_NOW } from './util'
import type { PickerDate } from './util'
import { TILL_NOW } from './util'

export type Precision = DatePrecision | WeekPrecision
export type Precision = DatePrecision | WeekPrecision | QuarterPrecision

export type DatePickerFilter = Partial<
Record<
Expand Down Expand Up @@ -35,6 +37,8 @@ export const convertDateToStringArray = (
) => {
if (precision.includes('week')) {
return weekUtils.convertDateToStringArray(date)
} else if (precision.includes('quarter')) {
return quarterUtils.convertDateToStringArray(date)
} else {
const datePrecision = precision as DatePrecision
const stringArray = dateUtils.convertDateToStringArray(date)
Expand All @@ -43,7 +47,7 @@ export const convertDateToStringArray = (
}

export const convertStringArrayToDate = <
T extends string | number | null | undefined
T extends string | number | null | undefined,
>(
value: T[],
precision: Precision
Expand All @@ -57,6 +61,8 @@ export const convertStringArrayToDate = <

if (precision.includes('week')) {
return weekUtils.convertStringArrayToDate(value)
} else if (precision.includes('quarter')) {
return quarterUtils.convertStringArrayToDate(value)
} else {
return dateUtils.convertStringArrayToDate(value)
}
Expand All @@ -80,6 +86,15 @@ export const generateDatePickerColumns = (
renderLabel,
filter
)
} else if (precision.startsWith('quarter')) {
return quarterUtils.generateDatePickerColumns(
selected,
min,
max,
precision as QuarterPrecision,
renderLabel,
filter
)
} else {
return dateUtils.generateDatePickerColumns(
selected,
Expand Down
45 changes: 45 additions & 0 deletions src/components/date-picker/demos/demo1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,47 @@ function DayOfWeekDemo() {
)
}

// 季度选择器
function QuarterDemo() {
const [visible, setVisible] = useState(false)

const labelRenderer = useCallback((type: string, data: number) => {
switch (type) {
case 'year':
return data + '年'
case 'quarter':
return data + '季度'
default:
return data
}
}, [])

return (
<>
<Button
onClick={() => {
setVisible(true)
}}
>
选择
</Button>
<DatePicker
visible={visible}
onClose={() => {
setVisible(false)
}}
defaultValue={now}
onConfirm={val => {
Toast.show(val.toDateString())
}}
onSelect={val => console.log(val)}
renderLabel={labelRenderer}
precision='quarter'
/>
</>
)
}

// 基础用法
function TillNowDemo() {
const [value, setValue] = useState(() => new Date())
Expand Down Expand Up @@ -252,6 +293,10 @@ export default () => {
<DayOfWeekDemo />
</DemoBlock>

<DemoBlock title='季度选择器'>
<QuarterDemo />
</DemoBlock>

<DemoBlock title='至今'>
<TillNowDemo />
</DemoBlock>
Expand Down
23 changes: 23 additions & 0 deletions src/components/date-picker/tests/date-picker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,29 @@ describe('DatePicker', () => {
expect(fn.mock.calls[0][0]).toEqual(today.toDateString())
})

test('precision quarter', async () => {
const today = new Date()
const fn = jest.fn()
const { getByText } = render(
<DatePicker
visible
precision='quarter'
value={now}
onConfirm={val => {
fn(val.toDateString())
}}
/>
)

expect(
document.body.querySelectorAll(`.${classPrefix}-view-column`).length
).toBe(2)
await waitFor(() => {
fireEvent.click(getByText('确定'))
})
expect(fn.mock.calls[0][0]).toEqual(today.toDateString())
})

test('test imperative call', async () => {
const today = new Date()
const fn = jest.fn()
Expand Down
2 changes: 1 addition & 1 deletion src/components/picker-view/index.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type PickerDate = Date & {
| min | Minimum value | `PickerDate` | ten years ago |
| mouseWheel | Whether to allow interact with mouse wheel | `boolean` | `false` |
| onChange | Triggered when the options are changed | `(value: PickerDate) => void` | - |
| precision | Precision | `'year' \| 'month' \| 'day' \| 'hour' \| 'minute' \| 'second' \| 'week' \| 'week-day'` | `'day'` |
| precision | Precision | `'year' \| 'month' \| 'day' \| 'hour' \| 'minute' \| 'second' \| 'week' \| 'week-day' \| 'quarter'` | `'day'` |
| renderLabel | The function to custom rendering the label shown on a column. `type` means any value in `precision`, `data` means the default number | `(type: string, data: number) => ReactNode` | - |
| value | Selected options | `PickerDate` | - |
| loading | Should the Picker displays as loading state | `boolean` | `false` |
Expand Down
2 changes: 1 addition & 1 deletion src/components/picker-view/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type PickerDate = Date & {
| min | 最小值 | `PickerDate` | 十年前 |
| mouseWheel | 是否允许通过鼠标滚轮进行选择 | `boolean` | `false` |
| onChange | 选项改变时触发 | `(value: PickerDate) => void` | - |
| precision | 精度 | `'year' \| 'month' \| 'day' \| 'hour' \| 'minute' \| 'second' \| 'week' \| 'week-day'` | `'day'` |
| precision | 精度 | `'year' \| 'month' \| 'day' \| 'hour' \| 'minute' \| 'second' \| 'week' \| 'week-day' \| 'quarter'` | `'day'` |
| renderLabel | 自定义渲染每列展示的内容。其中 `type` 参数为 `precision` 中的任意值,`data` 参数为默认渲染的数字 | `(type: string, data: number) => ReactNode` | - |
| value | 选中项 | `PickerDate` | - |
| loading | 是否处于加载状态 | `boolean` | `false` |
Expand Down
2 changes: 1 addition & 1 deletion src/components/picker/index.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ type PickerDate = Date & {
| mouseWheel | Whether to allow interact with mouse wheel | `boolean` | `false` |
| onConfirm | Triggered when confirming | `(value: PickerDate) => void` | - |
| onSelect | Triggered when the options are changed | `(value: PickerDate) => void` | - |
| precision | Precision | `'year' \| 'month' \| 'day' \| 'hour' \| 'minute' \| 'second' \| 'week' \| 'week-day'` | `'day'` |
| precision | Precision | `'year' \| 'month' \| 'day' \| 'hour' \| 'minute' \| 'second' \| 'week' \| 'week-day' \| 'quarter'` | `'day'` |
| renderLabel | The function to custom rendering the label shown on a column. `type` means any value in `precision` or `now`, `data` means the default number | `(type: Precision \| 'now', data: number) => ReactNode` | - |
| tillNow | Show till now in list | `boolean` | - | 5.27.0 |
| value | Selected value | `PickerDate` | - |
Expand Down
2 changes: 1 addition & 1 deletion src/components/picker/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ type PickerDate = Date & {
| mouseWheel | 是否允许通过鼠标滚轮进行选择 | `boolean` | `false` |
| onConfirm | 确认时触发 | `(value: PickerDate) => void` | - |
| onSelect | 选项改变时触发 | `(value: PickerDate) => void` | - |
| precision | 精度 | `'year' \| 'month' \| 'day' \| 'hour' \| 'minute' \| 'second' \| 'week' \| 'week-day'` | `'day'` |
| precision | 精度 | `'year' \| 'month' \| 'day' \| 'hour' \| 'minute' \| 'second' \| 'week' \| 'week-day' \| 'quarter'` | `'day'` |
| renderLabel | 自定义渲染每列展示的内容。其中 `type` 参数为 `precision` 中的任意值或 `now``data` 参数为默认渲染的数字 | `(type: Precision \| 'now', data: number) => ReactNode` | - |
| tillNow | 是否展示“至今” | `boolean` | - | 5.27.0 |
| value | 选中值 | `PickerDate` | - |
Expand Down

0 comments on commit dcbe575

Please sign in to comment.