diff --git a/cypress/e2e/spec.cy.js b/cypress/e2e/spec.cy.js index 3e2bd25..716fb43 100644 --- a/cypress/e2e/spec.cy.js +++ b/cypress/e2e/spec.cy.js @@ -76,4 +76,105 @@ describe('basic', () => { 'Nov 2030' ) }) + + it('should disable dates before min', function () { + cy.visit('localhost:8000') + + const today = new Date() + const tomorrow = new Date(today) + tomorrow.setDate(today.getDate() + 1) + + const formatDateInput = date => { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` + } + + cy.get('input[name="min"]') + .invoke('val', formatDateInput(tomorrow)) + .trigger('change') + + const todayISO = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + ).toISOString() + cy.get(`[data-date="${todayISO}"]`).should( + 'have.class', + 'preachjs-calendar--grid-cell-disabled' + ) + cy.get(`[data-date="${todayISO}"] > button`).click({ force: true }) + + cy.get('.selected-text').should('have.text', 'Selected: None') + }) + + it('should disable dates after max', function () { + cy.visit('localhost:8000') + + const today = new Date() + const yesterday = new Date(today) + yesterday.setDate(today.getDate() - 1) + + const formatDateInput = date => { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` + } + + cy.get('input[name="max"]') + .invoke('val', formatDateInput(yesterday)) + .trigger('change') + + const todayISO = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + ).toISOString() + cy.get(`[data-date="${todayISO}"]`).should( + 'have.class', + 'preachjs-calendar--grid-cell-disabled' + ) + cy.get(`[data-date="${todayISO}"] > button`).click({ force: true }) + + cy.get('.selected-text').should('have.text', 'Selected: None') + }) + + it('should allow selection within min and max', function () { + cy.visit('localhost:8000') + + const today = new Date() + const yesterday = new Date(today) + yesterday.setDate(today.getDate() - 1) + const tomorrow = new Date(today) + tomorrow.setDate(today.getDate() + 1) + + const formatDateInput = date => { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` + } + + cy.get('input[name="min"]') + .invoke('val', formatDateInput(yesterday)) + .trigger('change') + cy.get('input[name="max"]') + .invoke('val', formatDateInput(tomorrow)) + .trigger('change') + + const todayISO = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + ).toISOString() + cy.get(`[data-date="${todayISO}"]`).should( + 'not.have.class', + 'preachjs-calendar--grid-cell-disabled' + ) + cy.get(`[data-date="${todayISO}"] > button`).click() + + cy.get('.selected-text').should('not.have.text', 'Selected: None') + }) }) diff --git a/example/src/main.js b/example/src/main.js index b620bcb..32ea691 100644 --- a/example/src/main.js +++ b/example/src/main.js @@ -6,6 +6,22 @@ import './main.css' const mode = signal('single') const selectedDate = signal() const readOnly = signal(false) +const minDate = signal() +const maxDate = signal() + +function parseDateInput(value) { + if (!value) return undefined + const [year, month, day] = value.split('-').map(Number) + return new Date(year, month - 1, day) +} + +function formatDateInput(date) { + if (!date) return '' + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} const App = () => { return ( @@ -43,11 +59,35 @@ const App = () => { />{' '} Read Only + + { selectedDate.value = value }} diff --git a/package.json b/package.json index 65b23be..0c24f8a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "package:lint": "publint", "example:build": "node build.js", "example:dev": "node build.js --dev", - "test:e2e": "node ./script/e2e-run.js", + "test:e2e": "node ./scripts/e2e-run.js", "test": "echo true", "next": "bumpp", "fix": "prettier --write .", diff --git a/readme.md b/readme.md index 793d552..8d6dfaf 100644 --- a/readme.md +++ b/readme.md @@ -70,6 +70,8 @@ function App() { | `arrowLeft` | Icon Rendered on the left of the month selector | `<` | | `arrowRight` | Icon Rendered on the right of the month selector | `>` | | `readOnly` | Change to readOnly mode, date selection will do nothing | `false` | +| `min` | Minimum selectable date (inclusive) | | +| `max` | Maximum selectable date (inclusive) | | ## License diff --git a/src/calendar.d.ts b/src/calendar.d.ts index d81836e..60bd68c 100644 --- a/src/calendar.d.ts +++ b/src/calendar.d.ts @@ -16,4 +16,6 @@ export type CalendarProps = { mode?: M readOnly: boolean onSelect?: (nextValue: CalendarValue) => void + min?: Date + max?: Date } diff --git a/src/calendar.js b/src/calendar.js index 170f86c..4056f93 100644 --- a/src/calendar.js +++ b/src/calendar.js @@ -25,6 +25,8 @@ export function Calendar({ weekdayFormat = 'narrow', arrowLeft: ArrowLeft = () => <><, arrowRight: ArrowRight = () => <>>, + min = undefined, + max = undefined, }) { const selecting = useRef(false) const refRange$ = useSignal([]) @@ -199,12 +201,18 @@ export function Calendar({ isRangeEnd && 'preachjs-calendar--grid-cell-end', ] - if (dateItem.previousMonth || dateItem.nextMonth) { + const isDisabled = + dateItem.previousMonth || + dateItem.nextMonth || + isOutOfRange(dateItem.date, min, max) + + if (isDisabled) { return ( max.getTime()) return true + return false +} + function updateDateFromParts(sourceDate, nextValue) { const nextDate = new Date(sourceDate) const nextMonth = nextValue.month ?? nextDate.getMonth()