diff --git a/src/components/NcDateTimePicker/NcDateTimePicker.vue b/src/components/NcDateTimePicker/NcDateTimePicker.vue index b8e091363f..3dc890eaf7 100644 --- a/src/components/NcDateTimePicker/NcDateTimePicker.vue +++ b/src/components/NcDateTimePicker/NcDateTimePicker.vue @@ -295,6 +295,7 @@ import NcTimezonePicker from '../NcTimezonePicker/NcTimezonePicker.vue' import { t } from '../../l10n.ts' import NcButton from '../NcButton/index.ts' import { getDateFormat, getDateTimeFormat, getMonthFormat, getTimeFormat, getWeekFormat, getYearFormat } from './format.ts' +import { checkForNonStandardTokens } from './formatValidation.ts' import useDateFnsLocale from './useDateFnsLocale.ts' type LibraryFormatOptions = VueDatePickerProps['format'] @@ -556,6 +557,20 @@ watch(dateFnsLocale, () => { flush: 'post', }) +watchEffect(() => { + if (typeof props.format !== 'string') { + return + } + const nonStandardTokens = checkForNonStandardTokens(props.format) + if (nonStandardTokens.length !== 0) { + let warning = `[NcDateTimePicker] The \`format\` property value "${props.format}" uses the non-standard formating tokens ${nonStandardTokens.join(', ')}.` + warning += ' They will be disabled in the future because they are only supported by the date-fns library.' + warning += ' Only use tokens from the Unicode Technical Standard #35.' + warning += ' See https://github.com/nextcloud-libraries/nextcloud-vue/issues/8931' + warn(warning) + } +}) + /** * The date (time) formatting to be used by the library. * We use the provided format if possible, otherwise we provide a localized formatting diff --git a/src/components/NcDateTimePicker/formatValidation.ts b/src/components/NcDateTimePicker/formatValidation.ts new file mode 100644 index 0000000000..f2fc09259e --- /dev/null +++ b/src/components/NcDateTimePicker/formatValidation.ts @@ -0,0 +1,42 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * List of tokens supported by date-fns but not Unicode Technical Standard #35. + * + * @see https://date-fns.org/v4.4.0/docs/format + */ +const NON_STANDARD_TOKENS_OF_DATE_FNS = new Set([ + 'i', + 'I', + 'R', + 't', + 'T', + 'o', + 'P', + 'p', +]) + +const QUOTE_CHAR = "'" + +/** + * Returns array of used non-standard tokens. + * + * @param format The format string. + */ +export function checkForNonStandardTokens(format: string): string[] { + const usedNonStandardTokens = new Set() + let isQuoted = false + for (const char of format) { + if (char === QUOTE_CHAR) { + isQuoted = !isQuoted + } else if (!isQuoted) { + if (NON_STANDARD_TOKENS_OF_DATE_FNS.has(char)) { + usedNonStandardTokens.add(char) + } + } + } + return [...usedNonStandardTokens] +} diff --git a/tests/unit/components/NcDateTimePicker/NcDateTimePicker.spec.js b/tests/unit/components/NcDateTimePicker/NcDateTimePicker.spec.js index d8cbf5f473..05933e0f72 100644 --- a/tests/unit/components/NcDateTimePicker/NcDateTimePicker.spec.js +++ b/tests/unit/components/NcDateTimePicker/NcDateTimePicker.spec.js @@ -102,5 +102,49 @@ describe('NcDateTimePicker.vue', () => { expect(wrapper.find('[data-test-id="open-time-picker-btn"]').exists()).toBe(isRendered) }) + + describe('Non-standard token handling', () => { + it('warns about non-standard formatting tokens', () => { + const warnHandler = vi.fn() + + mount(NcDateTimePicker, { + props: { format: 'Pp' }, + global: { + config: { warnHandler }, + }, + }) + + expect(warnHandler).toHaveBeenCalledOnce() + expect(warnHandler.mock.calls[0][0]).toEqual('[NcDateTimePicker] The `format` property value "Pp" uses the non-standard formating tokens P, p. They will be disabled in the future because they are only supported by the date-fns library. Only use tokens from the Unicode Technical Standard #35. See https://github.com/nextcloud-libraries/nextcloud-vue/issues/8931') + }) + + it('warns about non-standard formatting tokens after property change', async () => { + const warnHandler = vi.fn() + const wrapper = mount(NcDateTimePicker, { + props: { + format: 'HH', + }, + global: { + config: { warnHandler }, + }, + }) + + await wrapper.setProps({ format: 'II-RR' }) + + expect(warnHandler).toHaveBeenCalledOnce() + expect(warnHandler.mock.calls[0][0]) + .toContain('[NcDateTimePicker] The `format` property value "II-RR" uses the non-standard formating tokens I, R.') + }) + + it('supports non-standard tokens', async () => { + const wrapper = mount(NcDateTimePicker, { + props: { modelValue: new Date(2026, 1, 10), format: 'II-RR' }, + }) + + await nextTick() + + expect(wrapper.find('input').element.value).toBe('07-2026') + }) + }) }) }) diff --git a/tests/unit/components/NcDateTimePicker/formatValidation.spec.ts b/tests/unit/components/NcDateTimePicker/formatValidation.spec.ts new file mode 100644 index 0000000000..c9d7f4caf1 --- /dev/null +++ b/tests/unit/components/NcDateTimePicker/formatValidation.spec.ts @@ -0,0 +1,31 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { describe, expect, it } from 'vitest' +import { checkForNonStandardTokens } from '../../../../src/components/NcDateTimePicker/formatValidation.ts' + +describe('checkForNonStandardTokens', () => { + it.for([ + "''", + "h 'o''clock'", + "yyyy''", + 'hh@mm', + ])('detects in %s no non-standard tokens', (format) => { + const tokens = checkForNonStandardTokens(format) + + expect(tokens).toHaveLength(0) + }) + + const casesWithNonStandardTokens = [ + ['II-RR', ['I', 'R']], + ['iIRtToPp', ['i', 'I', 'R', 't', 'T', 'o', 'P', 'p']], + ] as const + + it.for(casesWithNonStandardTokens)('detects in %s non-standard tokens %j ', ([format, nonStandardTokens]) => { + const tokens = checkForNonStandardTokens(format) + + expect(tokens).toEqual(nonStandardTokens) + }) +})