Skip to content

feat(b-form-datepicker, b-form-timepicker): add support for icon button only mode (closes #4888) #4915

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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