Skip to content

Commit

Permalink
feat(b-form-datepicker, b-form-timepicker): add support for icon butt…
Browse files Browse the repository at this point in the history
…on only mode (closes #4888) (#4915)

Co-authored-by: Jacob Müller
  • Loading branch information
tmorehouse authored Mar 10, 2020
1 parent 1d957eb commit 13660c3
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 10 deletions.
55 changes: 54 additions & 1 deletion src/components/form-datepicker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,58 @@ component's value.
Want a fancy popup with a dark background instead of a light background? Set the `dark` prop to
`true` to enable the dark background.

### Button only mode

<span class="badge badge-info small">v2.7.0+</span>

Fancy just a button that launches the date picker dialog, or want to provide your own optional text
input field? Use the `button-only` prop to render the datepicker as a dropdown button. The formatted
date label will be rendered with the class `sr-only` (available only to screen readers).

In the following simple example, we are placing the datepicker (button only mode) as an append to a
`<b-input-group>`:

```html
<template>
<div>
<label for="example-input">Choose a date</label>
<b-input-group class="mb-3">
<b-form-input
id="example-input"
v-model="value"
type="text"
placeholder="YYYY-MM-DD"
></b-form-input>
<b-input-group-append>
<b-form-datepicker
v-model="value"
button-only
right
locale="en-US"
aria-controls="example-input"
></b-form-datepicker>
</b-input-group-append">
</b-input-group>
<p>Value: '{{ value }}'</p>
</div>
</template>
<script>
export default {
data() {
return {
value: ''
}
}
}
</script>
<!-- b-form-datepicker-button-only.vue -->
```
Control the size of the button via the `size` prop, and the button variant via the `button-variant`
prop.
### Date string format
<span class="badge badge-info small">v2.6.0+</span>
Expand All @@ -331,7 +383,8 @@ properties for the `Intl.DateTimeFormat` object (see also
:date-format-options="{ year: 'numeric', month: 'short', day: '2-digit', weekday: 'short' }"
locale="en"
></b-form-datepicker>
<label for="datepicker-dateformat2">Short date format</label>
<label class="mt-3" for="datepicker-dateformat2">Short date format</label>
<b-form-datepicker
id="datepicker-dateformat2"
:date-format-options="{ year: 'numeric', month: 'numeric', day: 'numeric' }"
Expand Down
14 changes: 13 additions & 1 deletion src/components/form-datepicker/form-datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ const propsMixin = {
type: String,
default: null
},
buttonOnly: {
type: Boolean,
default: false
},
buttonVariant: {
// Applicable in button only mode
type: String,
default: 'secondary'
},
calendarWidth: {
// Width of the calendar dropdown
type: String,
Expand Down Expand Up @@ -310,7 +319,10 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
this.localYMD = formatYMD(newVal) || ''
},
localYMD(newVal) {
this.$emit('input', this.valueAsDate ? parseYMD(newVal) || null : newVal || '')
// We only update the v-model when the datepicker is open
if (this.isVisible) {
this.$emit('input', this.valueAsDate ? parseYMD(newVal) || null : newVal || '')
}
},
calendarYM(newVal, oldVal) /* istanbul ignore next */ {
// Displayed calendar month has changed
Expand Down
45 changes: 45 additions & 0 deletions src/components/form-datepicker/form-datepicker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ describe('form-date', () => {
await waitRAF()

expect(wrapper.classes()).toContain('b-form-datepicker')
expect(wrapper.classes()).toContain('b-form-btn-label-control')
expect(wrapper.classes()).toContain('form-control')
expect(wrapper.classes()).toContain('dropdown')
expect(wrapper.classes()).not.toContain('show')
expect(wrapper.classes()).not.toContain('btn-group')
expect(wrapper.attributes('role')).toEqual('group')

expect(wrapper.find('.dropdown-menu').exists()).toBe(true)
Expand All @@ -54,6 +56,7 @@ describe('form-date', () => {

expect(wrapper.find('label.form-control').exists()).toBe(true)
expect(wrapper.find('label.form-control').attributes('for')).toEqual('test-base')
expect(wrapper.find('label.form-control').classes()).not.toContain('sr-only')

expect(wrapper.find('input[type="hidden"]').exists()).toBe(false)

Expand All @@ -66,6 +69,48 @@ describe('form-date', () => {
wrapper.destroy()
})

it('has expected base structure in button-only mode', async () => {
const wrapper = mount(BFormDatepicker, {
attachToDocument: true,
propsData: {
id: 'test-button-only',
buttonOnly: true
}
})

expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('div')).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()

expect(wrapper.classes()).toContain('b-form-datepicker')
expect(wrapper.classes()).not.toContain('b-form-btn-label-control')
expect(wrapper.classes()).not.toContain('form-control')
expect(wrapper.classes()).toContain('dropdown')
expect(wrapper.classes()).not.toContain('show')
expect(wrapper.classes()).toContain('btn-group')
expect(wrapper.attributes('role')).not.toEqual('group')

expect(wrapper.find('.dropdown-menu').exists()).toBe(true)
expect(wrapper.find('.dropdown-menu').classes()).not.toContain('show')
expect(wrapper.find('.dropdown-menu').attributes('role')).toEqual('dialog')
expect(wrapper.find('.dropdown-menu').attributes('aria-modal')).toEqual('false')

expect(wrapper.find('label.form-control').exists()).toBe(true)
expect(wrapper.find('label.form-control').attributes('for')).toEqual('test-button-only')
expect(wrapper.find('label.form-control').classes()).toContain('sr-only')

expect(wrapper.find('input[type="hidden"]').exists()).toBe(false)

const $btn = wrapper.find('button#test-button-only')
expect($btn.exists()).toBe(true)
expect($btn.attributes('aria-haspopup')).toEqual('dialog')
expect($btn.attributes('aria-expanded')).toEqual('false')
expect($btn.find('svg.bi-calendar').exists()).toBe(true)

wrapper.destroy()
})

it('renders hidden input when name prop is set', async () => {
const wrapper = mount(BFormDatepicker, {
attachToDocument: true,
Expand Down
10 changes: 10 additions & 0 deletions src/components/form-datepicker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@
"prop": "direction",
"description": "Set to the string 'rtl' or 'ltr' to explicitly force the calendar to render in right-to-left or left-ro-right (respectively) mode. Defaults to the resolved locale's directionality"
},
{
"prop": "buttonOnly",
"version": "2.7.0",
"description": "Renders the datepicker as a dropdown button instead of a form-control"
},
{
"prop": "buttonVariant",
"version": "2.7.0",
"description": "The button variant to use when in `button-only` mode. Has no effect if prop `button-only` is not set"
},
{
"prop": "calendarWidth",
"version": "2.6.0",
Expand Down
54 changes: 54 additions & 0 deletions src/components/form-timepicker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,60 @@ control the positioning of the popup calendar.
Refer to the [`<b-dropdown>` documentation](/docs/components/dropdown) for details on the effects
and usage of these props.

### Button only mode

<span class="badge badge-info small">v2.7.0+</span>

Fancy just a button that launches the timepicker dialog, or want to provide your own optional text
input field? Use the `button-only` prop to render the timepicker as a dropdown button. The formatted
time label will be rendered with the class `sr-only` (available only to screen readers).

In the following simple example, we are placing the timepicker (button only mode) as an append to a
`<b-input-group>`:

```html
<template>
<div>
<label for="example-input">Choose a time</label>
<b-input-group class="mb-3">
<b-form-input
id="example-input"
v-model="value"
type="text"
placeholder="HH:mm:ss"
></b-form-input>
<b-input-group-append>
<b-form-timepicker
v-model="value"
button-only
right
show-seconds
:hour12="false"
locale="en-US"
aria-controls="example-input"
></b-form-timepicker>
</b-input-group-append">
</b-input-group>
<p>Value: '{{ value }}'</p>
</div>
</template>
<script>
export default {
data() {
return {
value: ''
}
}
}
</script>
<!-- b-form-timepicker-button-only.vue -->
```
Control the size of the button via the `size` prop, and the button variant via the `button-variant`
prop.
## Internationalization
Internationalization of the time interface is provided via
Expand Down
16 changes: 15 additions & 1 deletion src/components/form-timepicker/form-timepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ const propsMixin = {
type: [Number, String],
default: 1
},
buttonOnly: {
type: Boolean,
default: false
},
buttonVariant: {
// Applicable in button only mode
type: String,
default: 'secondary'
},
nowButton: {
type: Boolean,
default: false
Expand Down Expand Up @@ -240,7 +249,12 @@ export const BFormTimepicker = /*#__PURE__*/ Vue.extend({
this.localHMS = newVal || ''
},
localHMS(newVal) {
this.$emit('input', newVal || '')
// We only update hte v-model value when the timepicker
// is open, to prevent cursor jumps when bound to a
// text input in button only mode
if (this.isVisible) {
this.$emit('input', newVal || '')
}
}
},
methods: {
Expand Down
45 changes: 45 additions & 0 deletions src/components/form-timepicker/form-timepicker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ describe('form-timepicker', () => {
await waitRAF()

expect(wrapper.classes()).toContain('b-form-timepicker')
expect(wrapper.classes()).toContain('b-form-btn-label-control')
expect(wrapper.classes()).toContain('form-control')
expect(wrapper.classes()).toContain('dropdown')
expect(wrapper.classes()).not.toContain('show')
expect(wrapper.classes()).not.toContain('btn-group')
expect(wrapper.attributes('role')).toEqual('group')

expect(wrapper.find('.dropdown-menu').exists()).toBe(true)
Expand All @@ -66,6 +68,49 @@ describe('form-timepicker', () => {
wrapper.destroy()
})

it('has expected default structure when button-only is true', async () => {
const wrapper = mount(BFormTimepicker, {
attachToDocument: true,
propsData: {
id: 'test-button-only',
buttonOnly: true
}
})

expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('div')).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()

expect(wrapper.classes()).toContain('b-form-timepicker')
expect(wrapper.classes()).not.toContain('b-form-btn-label-control')
expect(wrapper.classes()).not.toContain('form-control')
expect(wrapper.classes()).toContain('dropdown')
expect(wrapper.classes()).not.toContain('show')
expect(wrapper.classes()).toContain('btn-group')
expect(wrapper.attributes('role')).not.toEqual('group')

expect(wrapper.find('.dropdown-menu').exists()).toBe(true)
expect(wrapper.find('.dropdown-menu').classes()).not.toContain('show')
expect(wrapper.find('.dropdown-menu').attributes('role')).toEqual('dialog')
expect(wrapper.find('.dropdown-menu').attributes('aria-modal')).toEqual('false')

expect(wrapper.find('label.form-control').exists()).toBe(true)
expect(wrapper.find('label.form-control').attributes('for')).toEqual('test-button-only')
expect(wrapper.find('label.form-control').text()).toContain('No time selected')
expect(wrapper.find('label.form-control').classes()).toContain('sr-only')

expect(wrapper.find('input[type="hidden"]').exists()).toBe(false)

const $btn = wrapper.find('button#test-button-only')
expect($btn.exists()).toBe(true)
expect($btn.attributes('aria-haspopup')).toEqual('dialog')
expect($btn.attributes('aria-expanded')).toEqual('false')
expect($btn.find('svg.bi-clock').exists()).toBe(true)

wrapper.destroy()
})

it('renders hidden input when name prop is set', async () => {
const wrapper = mount(BFormTimepicker, {
attachToDocument: true,
Expand Down
10 changes: 10 additions & 0 deletions src/components/form-timepicker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@
"prop": "hideHeader",
"description": "When set, visually hides the selected date header"
},
{
"prop": "buttonOnly",
"version": "2.7.0",
"description": "Renders the datepicker as a dropdown button instead of a form-control"
},
{
"prop": "buttonVariant",
"version": "2.7.0",
"description": "The button variant to use when in `button-only` mode. Has no effect if prop `button-only` is not set"
},
{
"prop": "menuClass",
"description": "Class (or classes) to apply to to popup menu wrapper"
Expand Down
Loading

0 comments on commit 13660c3

Please sign in to comment.