Skip to content

Commit

Permalink
Overlay Options (#111)
Browse files Browse the repository at this point in the history
* Implemented method to programmatically toggle the calendar.

* Updated README.md

* Added comment.

* I think I have overlay defaultView working! Also added the inputmode="numeric" prop to the year overlay to trigger a numberic keyboard on mobile devices.

* Added tests for toggleOverlay and defaultView.

* Added tests for defaultView.

* Added defaultView documentation to the readme.

* Woops, forgot to add defaultView in the table of contents.
  • Loading branch information
qodesmith authored Jan 1, 2021
1 parent 552bbe8 commit 552384a
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 14 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Get a date with JavaScript! Or a daterange, but that's not a good pun. Datepicke
* [customDays](#customdays)
* [customMonths](#custommonths)
* [customOverlayMonths](#customoverlaymonths)
* [defaultView](#defaultView)
* [overlayButton](#overlaybutton)
* [overlayPlaceholder](#overlayplaceholder)
* [events](#events)
Expand Down Expand Up @@ -80,6 +81,7 @@ Get a date with JavaScript! Or a daterange, but that's not a good pun. Datepicke
* [show](#show)
* [hide](#hide)
* [Show / Hide "Gotcha"](#show--hide-gotcha)
* [toggleOverlay](#toggleOverlay)
* [getRange](#getrange) _(daterange only)_


Expand Down Expand Up @@ -408,6 +410,18 @@ const picker = datepicker('.some-input', {
* Default - The first 3 characters of each item in `customMonths`.


### defaultView

Want the overlay to be the default view when opening the calendar? This property is for you. Simply set this property to `'overlay'` and you're done. This is helpful if you want a month picker to be front and center.

```javascript
const picker = datepicker('.some-input', {defaultView: 'overlay'})
```

* Type - string (`'calendar'` or `'overlay'`)
* Default - `'calendar'`


### overlayButton

Custom text for the year overlay submit button.
Expand Down Expand Up @@ -800,6 +814,19 @@ button.addEventListener('click', e => {
```


### toggleOverlay

Call this method on the picker to programmatically toggle the overlay. This will only work if the calendar is showing!

```javascript
const picker = datepicker('.some-input')

// Click the input to show the calendar...

picker.toggleOverlay()
```


### getRange

This method is only available on [daterange](#using-as-a-daterange-picker) pickers. It will return an object with `start` and `end` properties whose values are JavaScript date objects representing what the user selected on both calendars.
Expand Down
133 changes: 122 additions & 11 deletions cypress/integration/defaultPropertiesAndBehavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ function checkPickerProperties(picker, isDaterange, id) {
// First get the dom element, then ensure it has the correct default value.
} else if (domElement) {
cy.get(selector).then(elements => {
expect(value, property).to.equal(defaultValue(elements))
expect(elements, selector).to.have.lengthOf(1)
expect(value, `(checkPickerProperties) ${property}`).to.equal(defaultValue(elements))
expect(elements, `(checkPickerProperties) ${selector}`).to.have.lengthOf(1)
})

// Ensure the value is a function.
} else if (isFunction) {
expect(value, property).to.be.a('function')
expect(value, `(checkPickerProperties) method<${property}>`).to.be.a('function')

// The property should have the correct default value.
} else if (deepEqual) {
expect(value, property).to.deep.equal(defaultValue)
expect(value, `(checkPickerProperties) ${property}`).to.deep.equal(defaultValue)
} else {
expect(value, property).to.equal(defaultValue)
expect(value, `(checkPickerProperties) ${property}`).to.equal(defaultValue)
}
}
}
Expand Down Expand Up @@ -220,6 +220,7 @@ function testDomStructure(pickerType, selectorObj) {
cy.get('@overlayYearInput').should('have.attr', 'class', 'qs-overlay-year')
cy.get('@overlayYearInput').should('have.attr', 'placeholder', '4-digit year')
cy.get('@overlayYearInput').should('have.prop', 'value', '')
cy.get('@overlayYearInput').should('have.attr', 'inputmode', 'numeric')

// calendar => overlay => overlayInputContainer => close
cy.get(selectors.common.overlayClose).should('have.length', 1 * multiplier) // Searching the whole document.
Expand Down Expand Up @@ -282,17 +283,20 @@ describe('Default properties and behavior', function() {
describe('Single instance', function() {
it('should have the correct properties and values', function() {
const picker = this.datepicker(singleDatepickerInput)
const pickerProperties = Object.keys(picker)
const trackedProperties = singleDatepickerProperties.reduce((acc, {property}) => ({...acc, [property]: true}), {})

singleDatepickerProperties.forEach(checkPickerProperties(picker))

// Ensure that only and all the properties are in the picker instance.
const pickerKeys = Object.keys(picker)
const numOfPropertiesExpected = singleDatepickerProperties.length
expect(pickerKeys).to.have.length(numOfPropertiesExpected)
// Ensure that all the properties on the picker are tracked in our test data.
pickerProperties.forEach(property => {
expect(trackedProperties, `(trackedProperties) ${property}`).to.haveOwnProperty(property)
})

// Ensure all the properties in our test data are on the instance.
singleDatepickerProperties.forEach(({ property }) => {
expect(picker, property).to.haveOwnProperty(property)
})

// singleDatepickerProperties.forEach(checkPickerProperties(picker))
})

it('should have the correct DOM structure', function() {
Expand Down Expand Up @@ -421,6 +425,21 @@ describe('Default properties and behavior', function() {
})
})

it('should show the overlay if "defaultView" is set to "overlay"', function() {
this.datepicker(singleDatepickerInput, {defaultView: 'overlay'})

cy.get(selectors.single.overlay).should('not.be.visible')
cy.get(singleDatepickerInput).click().then(() => {
cy.get(selectors.single.overlay).should('be.visible')
cy.get(`${selectors.single.overlay} .qs-close`).click().then(() => {
cy.get(selectors.single.overlay).should('not.be.visible')
cy.get('body').click()
cy.get(singleDatepickerInput).click()
cy.get(selectors.single.overlay).should('be.visible')
})
})
})

it('should focus the overlay year input', function() {
this.datepicker(singleDatepickerInput)

Expand Down Expand Up @@ -1195,6 +1214,36 @@ describe('Default properties and behavior', function() {
expect(pickerOptions.onMonthChange).to.be.called
})
})

describe('toggleOverlay', function() {
it('should be a function', function() {
const picker = this.datepicker(singleDatepickerInput)
expect(picker.toggleOverlay).to.be.a('function')
})

it('should toggle the overlay', function() {
const picker = this.datepicker(singleDatepickerInput)

cy.get(singleDatepickerInput).click()
cy.get(selectors.common.overlay).should('not.be.visible').then(() => {
picker.toggleOverlay()
cy.get(selectors.common.overlay).should('be.visible')
cy.wait(500).then(() => {
picker.toggleOverlay()
cy.get(selectors.common.overlay).should('not.be.visible')
})
})
})

it(`should do nothing when the calendar isn't shown`, function() {
const picker = this.datepicker(singleDatepickerInput)
picker.toggleOverlay()
cy.get(singleDatepickerInput).click().then(() => {
cy.wait(500)
cy.get(selectors.common.overlay).should('not.be.visible')
})
})
})
})
})

Expand Down Expand Up @@ -2846,6 +2895,68 @@ describe('Default properties and behavior', function() {
expect(pickerEndOptions.onMonthChange).to.be.called
})
})

describe('toggleOverlay', function() {
it('should be a function', function() {
const pickerStart = this.datepicker(daterangeInputStart, { id: 1 })
const pickerEnd = this.datepicker(daterangeInputEnd, { id: 1 })

expect(pickerStart.toggleOverlay).to.be.a('function')
expect(pickerEnd.toggleOverlay).to.be.a('function')
})

it('(start) should toggle the overlay', function() {
const pickerStart = this.datepicker(daterangeInputStart, { id: 1 })
this.datepicker(daterangeInputEnd, { id: 1 })

cy.get(daterangeInputStart).click()
cy.get(selectors.range.start.overlay).should('not.be.visible').then(() => {
pickerStart.toggleOverlay()
cy.get(selectors.range.start.overlay).should('be.visible')
cy.wait(500).then(() => {
pickerStart.toggleOverlay()
cy.get(selectors.range.start.overlay).should('not.be.visible')
})
})
})

it('(end) should toggle the overlay', function() {
this.datepicker(daterangeInputStart, { id: 1 })
const pickerEnd = this.datepicker(daterangeInputEnd, { id: 1 })

cy.get(daterangeInputEnd).click()
cy.get(selectors.range.end.overlay).should('not.be.visible').then(() => {
pickerEnd.toggleOverlay()
cy.get(selectors.range.end.overlay).should('be.visible')
cy.wait(500).then(() => {
pickerEnd.toggleOverlay()
cy.get(selectors.range.end.overlay).should('not.be.visible')
})
})
})

it(`(start) should do nothing when the calendar isn't shown`, function() {
const pickerStart = this.datepicker(daterangeInputStart, { id: 1 })
this.datepicker(daterangeInputEnd, { id: 1 })

pickerStart.toggleOverlay()
cy.get(daterangeInputStart).click().then(() => {
cy.wait(500)
cy.get(selectors.range.start.overlay).should('not.be.visible')
})
})

it(`(end) should do nothing when the calendar isn't shown`, function() {
this.datepicker(daterangeInputStart, { id: 1 })
const pickerEnd = this.datepicker(daterangeInputEnd, { id: 1 })

pickerEnd.toggleOverlay()
cy.get(daterangeInputEnd).click().then(() => {
cy.wait(500)
cy.get(selectors.range.end.overlay).should('not.be.visible')
})
})
})
})
})
})
20 changes: 20 additions & 0 deletions cypress/integration/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,26 @@ describe('Errors thrown by datepicker', function() {
expect(() => this.datepicker(singleDatepickerInput, { customMonths, customOverlayMonths }))
.not.to.throw()
})

it('should throw if "defaultView" is not the correct value', function() {
expect(() => this.datepicker(singleDatepickerInput, {defaultView: 'nope'}))
.to.throw('options.defaultView must either be "calendar" or "overlay".')
})

// This test is dependent upon `datepicker.remove()`.
it('should not throw if "defaultView" is the correct value', function() {
let picker
const noThrow1 = () => {
picker = this.datepicker(singleDatepickerInput, {defaultView: 'calendar'})
}
const noThrow2 = () => {
picker = this.datepicker(singleDatepickerInput, {defaultView: 'overlay'})
}

expect(noThrow1).not.to.throw()
picker.remove()
expect(noThrow2).not.to.throw()
})
})

describe('General Errors', function() {
Expand Down
8 changes: 8 additions & 0 deletions cypress/pickerProperties.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ const singleDatepickerProperties = [
defaultValue: days,
deepEqual: true,
},
{
property: 'defaultView',
defaultValue: 'calendar',
},
{
property: 'disableMobile',
defaultValue: false,
Expand Down Expand Up @@ -234,6 +238,10 @@ const singleDatepickerProperties = [
property: 'startDay',
defaultValue: 0,
},
{
property: 'toggleOverlay',
isFunction: true,
},
{
property: 'weekendIndices',
defaultValue: [6, 0],
Expand Down
1 change: 1 addition & 0 deletions sandbox/sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ window.test = () => {

window.single = datepicker('input', {
alwaysShow: 0,
defaultView: 'overlay',
})
}
31 changes: 28 additions & 3 deletions src/datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ function createInstance(selectorOrElement, opts) {
// Events will show a small circle on calendar days.
events: options.events || {},

defaultView: options.defaultView,



// Method to programmatically set the calendar's date.
Expand All @@ -325,6 +327,9 @@ function createInstance(selectorOrElement, opts) {
// Method to programmatically navigate the calendar
navigate: navigate,

// Method to programmatically toggle the overlay.
toggleOverlay: instanceToggleOverlay,



// Callback fired when a date is selected - triggered in `selectDay`.
Expand Down Expand Up @@ -664,6 +669,13 @@ function sanitizeOptions(opts) {
if (typeof overlayPlaceholder !== 'string') delete options.overlayPlaceholder
if (typeof overlayButton !== 'string') delete options.overlayButton

// Show either the calendar (default) or the overlay when the calendar is open.
var defaultView = options.defaultView
if (defaultView && (defaultView !== 'calendar' && defaultView !== 'overlay')) {
throw new Error('options.defaultView must either be "calendar" or "overlay".')
}
options.defaultView = defaultView || 'calendar'

return options
}

Expand All @@ -673,7 +685,8 @@ function sanitizeOptions(opts) {
function defaults() {
return {
startDate: stripTime(new Date()),
position: 'bl'
position: 'bl',
defaultView: 'calendar',
}
}

Expand Down Expand Up @@ -906,7 +919,7 @@ function createOverlay(instance, overlayOpen) {
return [
'<div class="qs-overlay' + (overlayOpen ? '' : ' qs-hidden') + '">',
'<div>',
'<input class="qs-overlay-year" placeholder="' + overlayPlaceholder + '" />',
'<input class="qs-overlay-year" placeholder="' + overlayPlaceholder + '" inputmode="numeric" />',
'<div class="qs-close">&#10005;</div>',
'</div>',
'<div class="qs-overlay-month-container">' + shortMonths + '</div>',
Expand Down Expand Up @@ -1122,7 +1135,7 @@ function hideCal(instance) {
var isShowing = !instance.calendarContainer.classList.contains('qs-hidden')

if (isShowing && !instance.alwaysShow) {
toggleOverlay(true, instance)
instance.defaultView !== 'overlay' && toggleOverlay(true, instance)
instance.calendarContainer.classList.add('qs-hidden')
instance.onHide(instance)
}
Expand All @@ -1135,6 +1148,7 @@ function showCal(instance) {
if (instance.disabled) return

instance.calendarContainer.classList.remove('qs-hidden')
instance.defaultView === 'overlay' && toggleOverlay(false, instance)
calculatePosition(instance)
instance.onShow(instance)
}
Expand Down Expand Up @@ -1720,5 +1734,16 @@ function navigate(dateOrNum, triggerCb) {
}
}

/*
* Programmatically toggles the overlay.
* Only works when the calendar is open.
*/
function instanceToggleOverlay() {
var calendarIsShowing = !this.calendarContainer.classList.contains('qs-hidden')
var overlayIsShowing = !this.calendarContainer.querySelector('.qs-overlay').classList.contains('qs-hidden')

calendarIsShowing && toggleOverlay(overlayIsShowing, this)
}


export default datepicker
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = (env, argv) => ({
devServer: {
open: true,
contentBase: path.resolve(__dirname, './sandbox'),
host: '0.0.0.0', // Allow viewing site locally on a phone.
port: 9001,
public: 'http://localhost:9001'
},
Expand Down

0 comments on commit 552384a

Please sign in to comment.