diff --git a/src/components/form-datepicker/README.md b/src/components/form-datepicker/README.md
index 6c25c05716d..ce8ddf5c01e 100644
--- a/src/components/form-datepicker/README.md
+++ b/src/components/form-datepicker/README.md
@@ -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
+
+v2.7.0+
+
+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
+``:
+
+```html
+
+
+
+
+
+
+
+
+
+
Value: '{{ value }}'
+
+
+
+
+
+
+```
+
+Control the size of the button via the `size` prop, and the button variant via the `button-variant`
+prop.
+
### Date string format
v2.6.0+
@@ -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"
>
-
+
+
{
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)
@@ -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)
@@ -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,
diff --git a/src/components/form-datepicker/package.json b/src/components/form-datepicker/package.json
index 073a2515dbb..b0f3f7fffab 100644
--- a/src/components/form-datepicker/package.json
+++ b/src/components/form-datepicker/package.json
@@ -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",
diff --git a/src/components/form-timepicker/README.md b/src/components/form-timepicker/README.md
index 300eda4d93e..a389323f862 100644
--- a/src/components/form-timepicker/README.md
+++ b/src/components/form-timepicker/README.md
@@ -208,6 +208,60 @@ control the positioning of the popup calendar.
Refer to the [`` documentation](/docs/components/dropdown) for details on the effects
and usage of these props.
+### Button only mode
+
+v2.7.0+
+
+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
+``:
+
+```html
+
+
+
+
+
+
+
+
+
+
Value: '{{ value }}'
+
+
+
+
+
+
+```
+
+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
diff --git a/src/components/form-timepicker/form-timepicker.js b/src/components/form-timepicker/form-timepicker.js
index b15f8a12032..d36fec0e76f 100644
--- a/src/components/form-timepicker/form-timepicker.js
+++ b/src/components/form-timepicker/form-timepicker.js
@@ -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
@@ -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: {
diff --git a/src/components/form-timepicker/form-timepicker.spec.js b/src/components/form-timepicker/form-timepicker.spec.js
index fb27cd9c75d..b6fcd475eb7 100644
--- a/src/components/form-timepicker/form-timepicker.spec.js
+++ b/src/components/form-timepicker/form-timepicker.spec.js
@@ -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)
@@ -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,
diff --git a/src/components/form-timepicker/package.json b/src/components/form-timepicker/package.json
index 92342824416..4e006db1f10 100644
--- a/src/components/form-timepicker/package.json
+++ b/src/components/form-timepicker/package.json
@@ -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"
diff --git a/src/utils/bv-form-btn-label-control.js b/src/utils/bv-form-btn-label-control.js
index a4da96620b8..e44fe4cffca 100644
--- a/src/utils/bv-form-btn-label-control.js
+++ b/src/utils/bv-form-btn-label-control.js
@@ -83,6 +83,16 @@ export const BVFormBtnLabelControl = /*#__PURE__*/ Vue.extend({
// Vue coerces `undefined` into Boolean `false`
default: null
},
+ buttonOnly: {
+ // When true, renders a btn-group wrapper and visually hides the label
+ type: Boolean,
+ default: false
+ },
+ buttonVariant: {
+ // Applicable in button mode only
+ type: String,
+ default: 'secondary'
+ },
menuClass: {
// Extra classes to apply to the `dropdown-menu` div
type: [String, Array, Object]
@@ -152,14 +162,26 @@ export const BVFormBtnLabelControl = /*#__PURE__*/ Vue.extend({
const size = this.size
const value = toString(this.value) || ''
const labelSelected = this.labelSelected
+ const buttonOnly = !!this.buttonOnly
+ const buttonVariant = this.buttonVariant
const btnScope = { isHovered, hasFocus, state, opened: visible }
const $button = h(
'button',
{
ref: 'toggle',
- staticClass: 'btn border-0 h-auto py-0',
- class: { [`btn-${size}`]: !!size },
+ staticClass: 'btn',
+ class: {
+ [`btn-${buttonVariant}`]: buttonOnly,
+ [`btn-${size}`]: !!size,
+ 'border-0': !buttonOnly,
+ 'h-auto': !buttonOnly,
+ 'py-0': !buttonOnly,
+ // `dropdown-toggle` is needed for proper
+ // corner rounding in button only mode
+ 'dropdown-toggle': buttonOnly,
+ 'dropdown-toggle-no-caret': buttonOnly
+ },
attrs: {
id: idButton,
type: 'button',
@@ -231,6 +253,8 @@ export const BVFormBtnLabelControl = /*#__PURE__*/ Vue.extend({
{
staticClass: 'form-control text-break text-wrap border-0 bg-transparent h-auto pl-1 m-0',
class: {
+ // Hidden in button only mode
+ 'sr-only': buttonOnly,
// Mute the text if showing the placeholder
'text-muted': !value,
[`form-control-${size}`]: !!size,
@@ -261,21 +285,27 @@ export const BVFormBtnLabelControl = /*#__PURE__*/ Vue.extend({
return h(
'div',
{
- staticClass:
- 'b-form-btn-label-control form-control dropdown d-flex p-0 h-auto align-items-stretch',
+ staticClass: 'dropdown',
class: [
this.directionClass,
{
+ 'btn-group': buttonOnly,
+ 'b-form-btn-label-control': !buttonOnly,
+ 'form-control': !buttonOnly,
+ [`form-control-${size}`]: !!size && !buttonOnly,
+ 'd-flex': !buttonOnly,
+ 'p-0': !buttonOnly,
+ 'h-auto': !buttonOnly,
+ 'align-items-stretch': !buttonOnly,
+ focus: hasFocus && !buttonOnly,
show: visible,
- focus: hasFocus,
- [`form-control-${size}`]: !!size,
'is-valid': state === true,
'is-invalid': state === false
}
],
attrs: {
id: idWrapper,
- role: 'group',
+ role: buttonOnly ? null : 'group',
lang: this.lang || null,
dir: this.computedDir,
'aria-disabled': disabled,