From 63ec5d792e56fab0d76288a96ab8dec59b729dac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2020 16:03:40 +0100 Subject: [PATCH 01/29] chore(deps): update devdependency rollup-plugin-babel to ^4.4.0 (#4904) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index fe28e86e618..1c1c6073165 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "prettier": "1.14.3", "require-context": "^1.1.0", "rollup": "^1.32.1", - "rollup-plugin-babel": "^4.3.3", + "rollup-plugin-babel": "^4.4.0", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", "sass-loader": "^8.0.2", diff --git a/yarn.lock b/yarn.lock index 64afaa97604..a97e679e116 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11855,10 +11855,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rollup-plugin-babel@^4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-4.3.3.tgz#7eb5ac16d9b5831c3fd5d97e8df77ba25c72a2aa" - integrity sha512-tKzWOCmIJD/6aKNz0H1GMM+lW1q9KyFubbWzGiOG540zxPPifnEAHTZwjo0g991Y+DyOZcLqBgqOdqazYE5fkw== +rollup-plugin-babel@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-4.4.0.tgz#d15bd259466a9d1accbdb2fe2fff17c52d030acb" + integrity sha512-Lek/TYp1+7g7I+uMfJnnSJ7YWoD58ajo6Oarhlex7lvUce+RCKRuGRSgztDO3/MF/PuGKmUL5iTHKf208UNszw== dependencies: "@babel/helper-module-imports" "^7.0.0" rollup-pluginutils "^2.8.1" From 9afe5739ba0ae917a02067714ac41069710d8886 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 8 Mar 2020 11:40:32 -0300 Subject: [PATCH 02/29] chore(docs): minor fixes to form-spinbutton meta jason (#4908) --- src/components/form-spinbutton/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/form-spinbutton/package.json b/src/components/form-spinbutton/package.json index 970457de105..83a80cfbaae 100644 --- a/src/components/form-spinbutton/package.json +++ b/src/components/form-spinbutton/package.json @@ -32,7 +32,7 @@ }, { "prop": "wrap", - "description": "When set, allows the value to wrap around when reading the minimum or maximum value" + "description": "When set, allows the value to wrap around when reaching the minimum or maximum value" }, { "prop": "locale", @@ -44,7 +44,7 @@ }, { "prop": "formatterFn", - "description": "A reference to a method to format the displayed value. It ss passed a single argument which is the current value" + "description": "A reference to a method to format the displayed value. It is passed a single argument which is the current value" }, { "prop": "required", From d567cebd0734a742936cc88fda3be3a089b6acd0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2020 13:50:29 -0300 Subject: [PATCH 03/29] chore(deps): update devdependency eslint-plugin-vue to ^6.2.2 (#4909) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1c1c6073165..0ee48fa12b6 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", - "eslint-plugin-vue": "^6.2.1", + "eslint-plugin-vue": "^6.2.2", "esm": "^3.2.25", "gh-pages": "^2.2.0", "highlight.js": "^9.18.1", diff --git a/yarn.lock b/yarn.lock index a97e679e116..06724303de0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5693,10 +5693,10 @@ eslint-plugin-standard@^4.0.1: resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== -eslint-plugin-vue@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-6.2.1.tgz#ca802df5c33146aed1e56bb21d250c1abb6120a3" - integrity sha512-MiIDOotoWseIfLIfGeDzF6sDvHkVvGd2JgkvjyHtN3q4RoxdAXrAMuI3SXTOKatljgacKwpNAYShmcKZa4yZzw== +eslint-plugin-vue@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-6.2.2.tgz#27fecd9a3a24789b0f111ecdd540a9e56198e0fe" + integrity sha512-Nhc+oVAHm0uz/PkJAWscwIT4ijTrK5fqNqz9QB1D35SbbuMG1uB6Yr5AJpvPSWg+WOw7nYNswerYh0kOk64gqQ== dependencies: natural-compare "^1.4.0" semver "^5.6.0" From 498a26219571bb6108aaa7134dc25c8e1ff6c98f Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Mon, 9 Mar 2020 17:53:20 -0300 Subject: [PATCH 04/29] fix(b-form-file): fix value prop validation when using directory mode (fixes #4912) (#4913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(b-form-file): fix value prop validation when using directory mode Fixes #4912 * Update form-file.js * Update form-file.js * Update form-file.js * Update form-file.js * Update form-file.js * Update form-file.js * Update form-file.js Co-authored-by: Jacob Müller --- src/components/form-file/form-file.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/form-file/form-file.js b/src/components/form-file/form-file.js index 367374b3a56..08384bede71 100644 --- a/src/components/form-file/form-file.js +++ b/src/components/form-file/form-file.js @@ -12,11 +12,17 @@ import formStateMixin from '../../mixins/form-state' import idMixin from '../../mixins/id' import normalizeSlotMixin from '../../mixins/normalize-slot' +// --- Constants --- + const NAME = 'BFormFile' const VALUE_EMPTY_DEPRECATED_MSG = 'Setting "value"/"v-model" to an empty string for reset is deprecated. Set to "null" instead.' +// --- Helper methods --- + +const isValidValue = value => isFile(value) || (isArray(value) && value.every(v => isValidValue(v))) + // @vue/component export const BFormFile = /*#__PURE__*/ Vue.extend({ name: NAME, @@ -34,17 +40,13 @@ export const BFormFile = /*#__PURE__*/ Vue.extend({ value: { type: [File, Array], default: null, - validator: val => { + validator: value => { /* istanbul ignore next */ - if (val === '') { + if (value === '') { warn(VALUE_EMPTY_DEPRECATED_MSG, NAME) return true } - return ( - isUndefinedOrNull(val) || - isFile(val) || - (isArray(val) && (val.length === 0 || val.every(isFile))) - ) + return isUndefinedOrNull(value) || isValidValue(value) } }, accept: { From 134d64d073bb64fecd74ffc521476bfd97a99fc0 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 10 Mar 2020 04:11:46 -0300 Subject: [PATCH 05/29] feat(`b-overlay`): new component `b-overlay` (#4907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(`b-overlay`): new component `b-overlay` * overlay files * Update index.js * Update index.d.ts * Update index.js * Update overlay.js * Update overlay.js * Update overlay.js * Update overlay.js * Update overlay.js * Update overlay.spec.js * Update overlay.js * Update overlay.spec.js * Update package.json * Update README.md * Update package.json * Update README.md * Update README.md * Update package.json * Update package.json * Update README.md * Update package.json * Update README.md * Update overlay.js * Update package.json * Update package.json * Update package.json * Update package.json * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update overlay.js * Update package.json * Update overlay.js * Update package.json * Update README.md * Update README.md * Update overlay.js * Update README.md * Update overlay.js * Update overlay.js * Update README.md * Update README.md * Update README.md * Update overlay.spec.js * Update overlay.spec.js * Update overlay.spec.js * Update overlay.spec.js * Update README.md * Update README.md * Update overlay.js * Update package.json * Update README.md * Update overlay.js * Update README.md * add fade transition support * Update overlay.js * Update README.md * Update overlay.spec.js * Update overlay.js * Update README.md * Update overlay.spec.js * Update overlay.spec.js * Update package.json * Update README.md * Update README.md * Update README.md * Update README.md * Update overlay.spec.js * Update README.md Co-authored-by: Jacob Müller --- src/components/index.d.ts | 1 + src/components/index.js | 2 + src/components/overlay/README.md | 415 +++++++++++++++++++++++++ src/components/overlay/index.d.ts | 11 + src/components/overlay/index.js | 8 + src/components/overlay/overlay.js | 183 +++++++++++ src/components/overlay/overlay.spec.js | 229 ++++++++++++++ src/components/overlay/package.json | 118 +++++++ src/index.js | 4 + 9 files changed, 971 insertions(+) create mode 100644 src/components/overlay/README.md create mode 100644 src/components/overlay/index.d.ts create mode 100644 src/components/overlay/index.js create mode 100644 src/components/overlay/overlay.js create mode 100644 src/components/overlay/overlay.spec.js create mode 100644 src/components/overlay/package.json diff --git a/src/components/index.d.ts b/src/components/index.d.ts index 83c7b4c26a1..3196ba98b3c 100644 --- a/src/components/index.d.ts +++ b/src/components/index.d.ts @@ -37,6 +37,7 @@ export * from './media' export * from './modal' export * from './nav' export * from './navbar' +export * from './overlay' export * from './pagination' export * from './pagination-nav' export * from './popover' diff --git a/src/components/index.js b/src/components/index.js index 10ad2b05c3f..87241c0794a 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -35,6 +35,7 @@ import { MediaPlugin } from './media' import { ModalPlugin } from './modal' import { NavPlugin } from './nav' import { NavbarPlugin } from './navbar' +import { OverlayPlugin } from './overlay' import { PaginationPlugin } from './pagination' import { PaginationNavPlugin } from './pagination-nav' import { PopoverPlugin } from './popover' @@ -84,6 +85,7 @@ export const componentsPlugin = /*#__PURE__*/ pluginFactory({ ModalPlugin, NavPlugin, NavbarPlugin, + OverlayPlugin, PaginationPlugin, PaginationNavPlugin, PopoverPlugin, diff --git a/src/components/overlay/README.md b/src/components/overlay/README.md new file mode 100644 index 00000000000..82cac97b67a --- /dev/null +++ b/src/components/overlay/README.md @@ -0,0 +1,415 @@ +# Overlay + +> BootstrapVue's custom `b-overlay` component is used to _visually obscure_ a particular element or +> component and its content. It signals to the user of a state change within the element or +> component and can be used for creating loaders, warnings/alerts, prompts, and more. + +`` can be used to obscure almost any thing. Example use cases would be forms, tables, +delete confirmation dialogs, or anywhere you need to signal that the application is busy performing +a background task, or to signal that a certain component is unavailable. + +The component `` was introduced in BootstrapVue version `v2.7.0`. + +## Overview + +`` can be used to overlay (wrap) an element or component (the default behaviour), or can +be placed as a descendant of a `position: relative` element +([non-wrapping mode](#non-wrapping-mode)). + +The overlay visibility is controlled vis the `show` prop. By default the overlay is _not_ shown. + +
+

+ Note that this component only visually obscures its content (or the page). Refer to the + Accessibility section below for additional + accessibility details and concerns. +

+
+ +**Default wrapping mode example:** + +```html + + + + + +``` + +## Options + +### Overlay backdrop color + +You can control the backdrop background color via the `variant` prop. The variant is translated into +one of Bootstrap's +[background variant utility classes](/docs/reference/color-variants#background-and-border-variants). +Control the opacity of the backdrop via the `opacity` prop (opacity values can range from `0` to +`1`). And background blurring can be controlled via the `blur` prop. + +```html + + + + + +``` + +As an alternative to the `variant` prop, you can specify a CSS color string value via the `bg-color` +prop. When a value is provided for `bg-color`, the `variant` prop value is ignored. + +**Notes:** + +- Background blurring is not available on some browsers (e.g. IE 11). +- Blurring requires that the opacity level be relatively high for the effect to be visible. + +### Fade transition + +By default, the overlay uses Bootstrap's fade transition when showing or hiding. You can disable the +fade transition via adding the `no-fade` prop to ``. + +### Default spinner styling + +The default overlay content is a [``](/docs/components/spinner) of type `'border'`. You +can control the appearance of the spinner via the following props: + +- `spinner-type`: Currently supported values are `'border'` (the default) or `'grow'`. +- `spinner-variant`: Variant theme color for the spinner. Default is `null` which inherits the + current font color. +- `spinner-small`: Set to `true` to render a small size spinner. + +```html + + + +``` + +### Overlay corner rounding + +By default, the overlay backdrop has square corners. If the content you are wrapping has rounded +corners, you can use the `rounded` prop to apply rounding to the overlay's corners to match the +obscured content's rounded corners. + +Possible values are: + +- `true` (or the empty string `''`) to apply default (medium) rounding +- `false` (the default) applies no rounding to the backdrop overlay +- `'sm'` for small rounded corners +- `'lg'` for large rounded corners +- `'pill'` for pill style rounded corners +- `'circle'` for circular (or oval) rounding +- `'top'` for rounding only the top two corners +- `'bottom'` for rounding only the bottom two corners +- `'left'` for rounding only the two left corners +- `'right'` for rounding only the two right corners + +```html + + + + + +``` + +### Custom overlay content + +Place custom content in the overlay (replacing the default spinner) via the optionally scoped slot +`overlay`. + +```html + + + + + +``` + +The following scope properties are available to the `overlay` slot: + +| Property | Description | +| ---------------- | ----------------------------------- | +| `spinnerVariant` | Value of the `spinner-variant` prop | +| `spinnerType` | Value of the `spinner-type` prop | +| `spinnerSmall` | Value of the `spinner-small` prop | + +When placing interactive content in the overlay, you should focus the container of the custom +content or one of the focusable controls in the overlay content for accessibility reasons. You can +listen for the `` `'shown'` event to know when the overlay content is available in the +document. + +### Overlay content centering + +By default the overlay content will be horizontally and vertically centered within the overlay +region. To disabled centering, set the `no-center` prop to `true`. + +### Non-wrapping mode + +By default, `` wraps the content of the default slot. In some cases you may want to +obscure a parent container. Use the `no-wrap` prop to disable rendering of the wrapping (and ignore +the default slot). Note that this requires that the ancestor element that is to be obscured to have +relative positioning (either via the utility class `'position-relative'`, or CSS style +`'position: relative;'`). + +```html + + + + + +``` + +Note that some of Bootstrap v4's component styles have relative positioning defined (e.g. cards, +cols, etc.). You may need to adjust the placement of `` is your markup. + +When in `no-wrap` mode, `` will not set the `aria-busy` attribute on the obscured +element. You may also want to use an `aria-live` region in your app that announces to screen reader +users that the page is busy. + +Refer to the [Accessibility section](#accessibility) below for additional details and concerns. + +#### Absolute vs fixed positioning for `no-wrap` + +In cases where you want to obscure the entire app or page, when using the `no-wrap` prop, you can +switch to viewport fixed positioning via setting the prop `fixed` on ``. Note that this +does not disable scrolling of the page, and note that any interactive elements on the page will +still be in the document tab sequence. + +You may also need to adjust the z-index of the overlay to ensure that the backdrop appears above all +other page elements. Use the `z-index` property to override the default z-index value. + +Refer to the [Accessibility section](#accessibility) below for additional details and concerns. + +## Accessibility + +When using the wrapping mode (prop `no-wrap` is not set), the wrapper will have the attribute +`aria-bus="true"` set, to allow screen reader users to know the element is in a busy or loading +state. When prop `no-wrap` is set, then the attribute will not be applied. + +Note that the overlay is visual only. You **must** disable any interactive elements (buttons, links, +etc.) when the overlay is showing, otherwise the obscured elements will still be reachable via +keyboard navigation (i.e. still in the document tab sequence). It is also recommended to add either +the `aria-hidden="true"` or `aria-bus=y"true"` attribute to your obscured content when the overlay +is visible. Just be careful not to add `aria-hidden="true"` to the wrapper that contains the +`` component (when using `no-wrap`), as that would hide any interactive content in the +`overlay` slot for screen reader users. + +If you are placing interactive content in the `overlay` slot, you should focus the content once the +`'shown'` event has been emitted. + +When using the `no-wrap` prop, and potentially the `fixed` prop, to obscure the entire application +or page, you must ensure that internative page elements (other than the content of the overlay) have +been disabled and are not in the document tab sequence. diff --git a/src/components/overlay/index.d.ts b/src/components/overlay/index.d.ts new file mode 100644 index 00000000000..08e3cd503b4 --- /dev/null +++ b/src/components/overlay/index.d.ts @@ -0,0 +1,11 @@ +// +// Overlay +// +import Vue from 'vue' +import { BvPlugin, BvComponent } from '../../' + +// Plugin +export declare const OverlayPlugin: BvPlugin + +// Component: b-overlay +export declare class BOverlay extends BvComponent {} diff --git a/src/components/overlay/index.js b/src/components/overlay/index.js new file mode 100644 index 00000000000..dc568b1efcc --- /dev/null +++ b/src/components/overlay/index.js @@ -0,0 +1,8 @@ +import { BOverlay } from './overlay' +import { pluginFactory } from '../../utils/plugins' + +const OverlayPlugin = /*#__PURE__*/ pluginFactory({ + components: { BOverlay } +}) + +export { OverlayPlugin, BOverlay } diff --git a/src/components/overlay/overlay.js b/src/components/overlay/overlay.js new file mode 100644 index 00000000000..433a6dd2ca6 --- /dev/null +++ b/src/components/overlay/overlay.js @@ -0,0 +1,183 @@ +import Vue from '../../utils/vue' +import { BVTransition } from '../../utils/bv-transition' +import { toFloat } from '../../utils/number' +import normalizeSlotMixin from '../../mixins/normalize-slot' +import { BSpinner } from '../spinner/spinner' + +const positionCover = { top: 0, left: 0, bottom: 0, right: 0 } + +export const BOverlay = /*#__PURE__*/ Vue.extend({ + name: 'BOverlay', + mixins: [normalizeSlotMixin], + props: { + show: { + type: Boolean, + default: false + }, + variant: { + type: String, + default: 'light' + }, + bgColor: { + // Alternative to variant, allowing a specific + // CSS color to be applied to the overlay + type: String, + default: null + }, + opacity: { + type: [Number, String], + default: 0.85, + validator(value) { + const number = toFloat(value) + return number >= 0 && number <= 1 + } + }, + blur: { + type: String, + default: '2px' + }, + rounded: { + type: [Boolean, String], + default: false + }, + noCenter: { + type: Boolean, + default: false + }, + noFade: { + type: Boolean, + default: false + }, + spinnerType: { + type: String, + default: 'border' + }, + spinnerVariant: { + type: String, + default: null + }, + spinnerSmall: { + type: Boolean, + default: false + }, + overlayTag: { + type: String, + default: 'div' + }, + wrapTag: { + type: String, + default: 'div' + }, + noWrap: { + // If set, does not render the default slot + // and switches to absolute positioning + type: Boolean, + default: false + }, + fixed: { + type: Boolean, + default: false + }, + zIndex: { + type: [Number, String], + default: 10 + } + }, + computed: { + computedRounded() { + const rounded = this.rounded + return rounded === true || rounded === '' ? 'rounded' : !rounded ? '' : `rounded-${rounded}` + }, + computedVariant() { + return this.variant && !this.bgColor ? `bg-${this.variant}` : '' + }, + overlayScope() { + return { + spinnerType: this.spinnerType, + spinnerVariant: this.spinnerVariant || null, + spinnerSmall: this.spinnerSmall + } + } + }, + methods: { + defaultOverlayFn({ spinnerType, spinnerVariant, spinnerSmall }) { + return this.$createElement(BSpinner, { + props: { + type: spinnerType, + variant: spinnerVariant, + small: spinnerSmall + } + }) + } + }, + render(h) { + let $overlay = h() + if (this.show) { + const scope = this.overlayScope + // Overlay backdrop + const $background = h('div', { + staticClass: 'position-absolute', + class: [this.computedVariant, this.computedRounded], + style: { + ...positionCover, + opacity: this.opacity, + backgroundColor: this.bgColor || null, + backdropFilter: this.blur ? `blur(${this.blur})` : null + } + }) + // Overlay content + const $content = h( + 'div', + { + staticClass: 'position-absolute', + style: this.noCenter + ? { ...positionCover } + : { top: '50%', left: '50%', transform: 'translateX(-50%) translateY(-50%)' } + }, + [this.normalizeSlot('overlay', scope) || this.defaultOverlayFn(scope)] + ) + // Overlay positioning + $overlay = h( + this.overlayTag, + { + key: 'overlay', + staticClass: 'b-overlay', + class: { + 'position-absolute': !this.noWrap || (this.noWrap && !this.fixed), + 'position-fixed': this.noWrap && this.fixed + }, + style: { ...positionCover, zIndex: this.zIndex || 10 } + }, + [$background, $content] + ) + } + // Wrap in a fade transition + $overlay = h( + BVTransition, + { + props: { + noFade: this.noFade, + appear: true + }, + on: { + 'after-enter': () => this.$emit('shown'), + 'after-leave': () => this.$emit('hidden') + } + }, + [$overlay] + ) + + if (this.noWrap) { + return $overlay + } + + return h( + this.wrapTag, + { + staticClass: 'b-overlay-wrap position-relative', + attrs: { 'aria-busy': this.show ? 'true' : null } + }, + this.noWrap ? [$overlay] : [this.normalizeSlot('default'), $overlay] + ) + } +}) diff --git a/src/components/overlay/overlay.spec.js b/src/components/overlay/overlay.spec.js new file mode 100644 index 00000000000..9918b33e01c --- /dev/null +++ b/src/components/overlay/overlay.spec.js @@ -0,0 +1,229 @@ +import { mount } from '@vue/test-utils' +import { waitNT, waitRAF } from '../../../tests/utils' +import { BOverlay } from './overlay' + +describe('overlay', () => { + it('has expected default structure', async () => { + const wrapper = mount(BOverlay, { + slots: { + default: 'foobar' + } + }) + + expect(wrapper.isVueInstance()).toBe(true) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).not.toBe('true') + expect(wrapper.text()).toContain('foobar') + expect(wrapper.find('.b-overlay').exists()).toBe(false) + expect(wrapper.find('.spinner-border').exists()).toBe(false) + + wrapper.destroy() + }) + + it('has expected default structure when `show` prop is true', async () => { + const wrapper = mount(BOverlay, { + propsData: { + show: true + }, + slots: { + default: 'foobar' + } + }) + + expect(wrapper.isVueInstance()).toBe(true) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).toBe('true') + expect(wrapper.text()).toContain('foobar') + + const $overlay = wrapper.find('.b-overlay') + expect($overlay.exists()).toBe(true) + expect($overlay.classes()).toContain('position-absolute') + + const $children = $overlay.findAll('div:not(.b-overlay)') + expect($children.length).toBe(2) + + expect($children.at(0).classes()).toContain('position-absolute') + expect($children.at(0).classes()).toContain('bg-light') + expect($children.at(0).text()).toBe('') + + expect($children.at(1).classes()).toContain('position-absolute') + expect($children.at(1).classes()).not.toContain('bg-light') + expect( + $children + .at(1) + .find('.spinner-border') + .exists() + ).toBe(true) + + wrapper.destroy() + }) + + it('responds to changes in the `show` prop', async () => { + const wrapper = mount(BOverlay, { + attachToDocument: true, + stubs: { + // Disable the use of transitionStub fake transition + // as it doesn't run transition hooks + transition: false + }, + propsData: { + show: false + }, + slots: { + default: 'foobar' + } + }) + + expect(wrapper.isVueInstance()).toBe(true) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).not.toBe('true') + expect(wrapper.text()).toContain('foobar') + expect(wrapper.find('.b-overlay').exists()).toBe(false) + expect(wrapper.find('.spinner-border').exists()).toBe(false) + + expect(wrapper.emitted('shown')).toBeUndefined() + expect(wrapper.emitted('hidden')).toBeUndefined() + + wrapper.setProps({ + show: true + }) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).toBe('true') + expect(wrapper.text()).toContain('foobar') + expect(wrapper.find('.b-overlay').exists()).toBe(true) + expect(wrapper.find('.spinner-border').exists()).toBe(true) + + expect(wrapper.emitted('shown')).not.toBeUndefined() + expect(wrapper.emitted('hidden')).toBeUndefined() + expect(wrapper.emitted('shown').length).toBe(1) + + wrapper.setProps({ + show: false + }) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('b-overlay-wrap') + expect(wrapper.classes()).toContain('position-relative') + expect(wrapper.attributes('aria-busy')).not.toBe('true') + expect(wrapper.text()).toContain('foobar') + expect(wrapper.find('.b-overlay').exists()).toBe(false) + expect(wrapper.find('.spinner-border').exists()).toBe(false) + + expect(wrapper.emitted('hidden')).not.toBeUndefined() + expect(wrapper.emitted('shown').length).toBe(1) + expect(wrapper.emitted('hidden').length).toBe(1) + + wrapper.setProps({ + show: true + }) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.emitted('shown').length).toBe(2) + expect(wrapper.emitted('hidden').length).toBe(1) + + wrapper.setProps({ + show: false + }) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.emitted('shown').length).toBe(2) + expect(wrapper.emitted('hidden').length).toBe(2) + + wrapper.destroy() + }) + + it('has expected default structure when `no-wrap` is set', async () => { + const wrapper = mount(BOverlay, { + propsData: { + noWrap: true + } + }) + + expect(wrapper.isVueInstance()).toBe(true) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.is('div')).toBe(undefined) + + wrapper.destroy() + }) + + it('has expected default structure when `no-wrap` is set and `show` is true', async () => { + const wrapper = mount(BOverlay, { + propsData: { + noWrap: true, + show: true + } + }) + + expect(wrapper.isVueInstance()).toBe(true) + await waitNT(wrapper.vm) + await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('b-overlay') + expect(wrapper.classes()).toContain('position-absolute') + expect(wrapper.classes()).not.toContain('b-overlay-wrap') + expect(wrapper.classes()).not.toContain('position-relative') + + const $children = wrapper.findAll('div:not(.b-overlay)') + expect($children.length).toBe(2) + + expect($children.at(0).classes()).toContain('position-absolute') + expect($children.at(0).classes()).toContain('bg-light') + expect($children.at(0).text()).toBe('') + + expect($children.at(1).classes()).toContain('position-absolute') + expect($children.at(1).classes()).not.toContain('bg-light') + expect( + $children + .at(1) + .find('.spinner-border') + .exists() + ).toBe(true) + + wrapper.destroy() + }) +}) diff --git a/src/components/overlay/package.json b/src/components/overlay/package.json new file mode 100644 index 00000000000..ff9ab4fc085 --- /dev/null +++ b/src/components/overlay/package.json @@ -0,0 +1,118 @@ +{ + "name": "@bootstrap-vue/overlay", + "version": "1.0.0", + "meta": { + "title": "Overlay", + "new": true, + "description": "The b-overlay component is used to visually obscure a particular element or component and its content. It signals to the user of a state change within the element or component and can be used for creating loaders, warnings/alerts and more.", + "components": [ + { + "component": "BOverlay", + "version": "2.7.0", + "props": [ + { + "prop": "show", + "description": "When set, shows the overlay" + }, + { + "prop": "variant", + "description": "Backgrund variant to use for the overlay backdrop" + }, + { + "prop": "bgColor", + "description": "CSS color to use as the opaque overlay backdrop color. If set, overrides the `variant` prop" + }, + { + "prop": "opacity", + "description": "Opacity of the overlay backdrop. Valid range is `0` to `1`" + }, + { + "prop": "blur", + "description": "Value for the CSS blur backdrop-filter. Be sure to include the CSS units. Not supported in IE 11. Set to null or an empty string to disable blurring" + }, + { + "prop": "noFade", + "description": "Disables the fade transition of the overlay" + }, + { + "prop": "rounded", + "description": "Apply rounding to the overlay to match your content routing. Valid values are `true`, `'sm'`, `lg`, `circle`, `pill`, `top`, `right`, `bottom`, or `left`" + }, + { + "prop": "noCenter", + "description": "When set, disables the vertical and horizontal centering of the overlay content" + }, + { + "prop": "overlayTag", + "description": "Element tag to use as for the overlay element" + }, + { + "prop": "noWrap", + "description": "Disabled generating the wrapper element, and ignored the default slot. Requires that `` be placed in an element with position relative set" + }, + { + "prop": "fixed", + "description": "When prop `no-wrap` is set, will use fixed positioning instead of absolute positioning. Handy if you want to obscure the entire application page" + }, + { + "prop": "wrapTag", + "description": "Element tag to use for the overall wrapper element. Has no effect if prop `no-wrap` is set" + }, + { + "prop": "zIndex", + "description": "Z-index value to apply to the overlay. You may need to increase this value to suit your content" + }, + { + "prop": "spinnerType", + "description": "Type of the default spinner to show. Current supported types are 'border' and 'grow'" + }, + { + "prop": "spinnerVariant", + "description": "Applies one of the Bootstrap theme color variants to the default spinner" + }, + { + "prop": "spinnerSmall", + "description": "When set, renderes the default spinner in a smaller size" + } + ], + "events": [ + { + "event": "shown", + "description": "Emitted when the overlay has been shown" + }, + { + "event": "hidden", + "description": "Emitted when the overlay has been hidden" + } + ], + "slots": [ + { + "name": "overlay", + "description": "Custom content to replace the default overlay spinner", + "scope": [ + { + "prop": "spinnerType", + "type": "String", + "description": "" + }, + { + "prop": "spinnerVariant", + "type": "String", + "description": "" + }, + { + "prop": "spinnerSmall", + "type": "Boolean", + "description": "" + } + ] + }, + { + "name": "default", + "description": "The content to have overlay. The default slot is ignored if the prop `no-wrap` is set" + } + ] + } + ] + } +} diff --git a/src/index.js b/src/index.js index 898f370f051..639a3278c64 100644 --- a/src/index.js +++ b/src/index.js @@ -245,6 +245,10 @@ export { BNavbarBrand } from './components/navbar/navbar-brand' export { BNavbarNav } from './components/navbar/navbar-nav' export { BNavbarToggle } from './components/navbar/navbar-toggle' +// export * from './components/overlay' +export { OverlayPlugin } from './components/overlay' +export { BOverlay } from './components/overlay/overlay' + // export * from './components/pagination' export { PaginationPlugin } from './components/pagination' export { BPagination } from './components/pagination/pagination' From 1d957ebd78a8693e91a8116d12c28fe24bd7c19c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 10 Mar 2020 04:21:27 -0300 Subject: [PATCH 06/29] feat(b-calendar, b-for-datepicker): add new `initial-date` prop, and constrain today/current month buttons between `min` and `max` (closes #4899) (#4906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(b-calendar, b-for-datepicker): add new initial-date prop (closes #4899) * Update date.js * Update date.spec.js * Update date.spec.js * Update form-datepicker.js * Update date.js * Update calendar.js * Update date.js * Update form-datepicker.js * Update form-datepicker.js * Update package.json * Update package.json * Update README.md * Update README.md * Update README.md * Fix typos Co-authored-by: Jacob Müller --- src/components/calendar/README.md | 10 +++++- src/components/calendar/calendar.js | 31 +++++++++++------ src/components/calendar/package.json | 5 +++ src/components/form-datepicker/README.md | 12 +++++++ .../form-datepicker/form-datepicker.js | 22 ++++++++---- src/components/form-datepicker/package.json | 5 +++ src/components/form-timepicker/package.json | 2 +- src/components/time/package.json | 2 +- src/utils/date.js | 11 ++++++ src/utils/date.spec.js | 34 ++++++++++++++++++- 10 files changed, 114 insertions(+), 20 deletions(-) diff --git a/src/components/calendar/README.md b/src/components/calendar/README.md index fde90bef268..2debb6ab272 100644 --- a/src/components/calendar/README.md +++ b/src/components/calendar/README.md @@ -230,6 +230,14 @@ fit the width of the parent element. The `width` prop has no effect when `block` Note it is _not recommended_ to set a width below `260px`, otherwise truncation and layout issues with the component may occur. +### Initial open calendar date + +By default, when no date is selected, the calendar view will be set to the current month (or the +`min` or `max` date if today's date is out of range of `min` or `max`). You can change this +behaviour by specifying a date via the `initial-date` prop. The initial date prop will be used to +determine the calendar month to be initially presented to the user. It does not set the component's +value. + ### Date string format v2.6.0+ @@ -632,5 +640,5 @@ verbosity and to provide consistency across various screen readers (NVDA, when e ## See also - [`` Date picker custom form input](/docs/components/form-datepicker) -- [`` Time picker custom form input](/docs/comonents/form-timepicker) +- [`` Time picker custom form input](/docs/components/form-timepicker) - [`` Time date selection widget](/docs/components/calendar) diff --git a/src/components/calendar/calendar.js b/src/components/calendar/calendar.js index d9723e48a8d..8f7c1e48869 100644 --- a/src/components/calendar/calendar.js +++ b/src/components/calendar/calendar.js @@ -7,6 +7,7 @@ import { getComponentConfig } from '../../utils/config' import { createDate, createDateFormatter, + constrainDate, datesEqual, firstDateOfMonth, formatYMD, @@ -58,6 +59,13 @@ export const BCalendar = Vue.extend({ type: Boolean, default: false }, + initialDate: { + // This specifies the calendar year/month/day that will be shown when + // first opening the datepicker if no v-model value is provided + // Default is the current date (or `min`/`max`) + type: [String, Date], + default: null + }, disabled: { type: Boolean, default: false @@ -212,7 +220,9 @@ export const BCalendar = Vue.extend({ // Selected date selectedYMD: selected, // Date in calendar grid that has `tabindex` of `0` - activeYMD: selected || formatYMD(this.getToday()), + activeYMD: + selected || + formatYMD(constrainDate(this.initialDate || this.getToday()), this.min, this.max), // Will be true if the calendar grid has/contains focus gridHasFocus: false, // Flag to enable the `aria-live` region(s) after mount @@ -361,6 +371,7 @@ export const BCalendar = Vue.extend({ // Merge in user supplied options ...this.dateFormatOptions, // Ensure hours/minutes/seconds are not shown + // As we do not support the time portion (yet) hour: undefined, minute: undefined, second: undefined, @@ -487,7 +498,9 @@ export const BCalendar = Vue.extend({ }, hidden(newVal) { // Reset the active focused day when hidden - this.activeYMD = this.selectedYMD || formatYMD(this.value) || formatYMD(this.getToday()) + this.activeYMD = + this.selectedYMD || + formatYMD(this.value || this.constrainDate(this.initialDate || this.getToday())) // Enable/disable the live regions this.setLive(!newVal) } @@ -541,10 +554,7 @@ export const BCalendar = Vue.extend({ constrainDate(date) { // Constrains a date between min and max // returns a new `Date` object instance - date = parseYMD(date) - const min = this.computedMin || date - const max = this.computedMax || date - return createDate(date < min ? min : date > max ? max : date) + return constrainDate(date, this.computedMin, this.computedMax) }, emitSelected(date) { // Performed in a `$nextTick()` to (probably) ensure @@ -573,6 +583,7 @@ export const BCalendar = Vue.extend({ let activeDate = createDate(this.activeDate) let checkDate = createDate(this.activeDate) const day = activeDate.getDate() + const constrainedToday = this.constrainDate(this.getToday()) const isRTL = this.isRTL if (keyCode === PAGEUP) { // PAGEUP - Previous month/year @@ -605,11 +616,11 @@ export const BCalendar = Vue.extend({ checkDate = activeDate } else if (keyCode === HOME) { // HOME - Today - activeDate = this.getToday() + activeDate = constrainedToday checkDate = activeDate } else if (keyCode === END) { // END - Selected date, or today if no selected date - activeDate = parseYMD(this.selectedDate) || this.getToday() + activeDate = parseYMD(this.selectedDate) || constrainedToday checkDate = activeDate } if (!this.dateOutOfRange(checkDate) && !datesEqual(activeDate, this.activeDate)) { @@ -664,7 +675,7 @@ export const BCalendar = Vue.extend({ }, gotoCurrentMonth() { // TODO: Maybe this goto date should be configurable? - this.activeYMD = formatYMD(this.getToday()) + this.activeYMD = formatYMD(this.constrainDate(this.getToday())) }, gotoNextMonth() { this.activeYMD = formatYMD(this.constrainDate(oneMonthAhead(this.activeDate))) @@ -694,7 +705,7 @@ export const BCalendar = Vue.extend({ // Flag for making the `aria-live` regions live const isLive = this.isLive // Pre-compute some IDs - // Thes should be computed props + // This should be computed props const idValue = safeId() const idWidget = safeId('_calendar-wrapper_') const idNav = safeId('_calendar-nav_') diff --git a/src/components/calendar/package.json b/src/components/calendar/package.json index 6bf274e46bf..14f5d2544f3 100644 --- a/src/components/calendar/package.json +++ b/src/components/calendar/package.json @@ -20,6 +20,11 @@ "prop": "valueAsDate", "description": "Returns a `Date` object for the v-model instead of a `YYYY-MM-DD` string" }, + { + "prop": "initialDate", + "version": "2.7.0", + "description": "When a `value` is not specified, sets the initial calendar month date that will be presented to the user. Accepts a value in `YYYY-MM-DD` format or a `Date` object. Defaults to the current date (or min or max if the current date is out of range)" + }, { "prop": "disabled", "description": "Places the calendar in a non-interactive disabled state" diff --git a/src/components/form-datepicker/README.md b/src/components/form-datepicker/README.md index 322d1f77bca..6c25c05716d 100644 --- a/src/components/form-datepicker/README.md +++ b/src/components/form-datepicker/README.md @@ -288,6 +288,10 @@ The text for the optional buttons can be set via the `label-today-button`, `labe the `label-close-button` props. Due to the limited width of the footer section, it is recommended to keep these labels short. +Note that the `Set Today` button may not set the control today's date, if today's date is outside of +the `min` or `max` date range restrictions. In the case it is outside of the range, it will set to +either `min` or `max` (depending on which is closes to today's date). + ### Dropdown placement Use the dropdown props `right`, `dropup`, `dropright`, `dropleft`, `no-flip`, and `offset` to @@ -296,6 +300,14 @@ control the positioning of the popup calendar. Refer to the [`` documentation](/docs/components/dropdown) for details on the effects and usage of these props. +### Initial open calendar date + +By default, when no date is selected, the calendar view will be set to the current month (or the +`min` or `max` date if today's date is out of range of `min` or `max`) when opened. You can change +this behaviour by specifying a date via the `initial-date` prop. The initial date prop will be used +to determine the calendar month to be initially presented to the user. It does not set the +component's value. + ### Dark mode Want a fancy popup with a dark background instead of a light background? Set the `dark` prop to diff --git a/src/components/form-datepicker/form-datepicker.js b/src/components/form-datepicker/form-datepicker.js index 7fb362790a9..09c857426c5 100644 --- a/src/components/form-datepicker/form-datepicker.js +++ b/src/components/form-datepicker/form-datepicker.js @@ -1,7 +1,7 @@ import Vue from '../../utils/vue' import { BVFormBtnLabelControl, dropdownProps } from '../../utils/bv-form-btn-label-control' import { getComponentConfig } from '../../utils/config' -import { createDate, formatYMD, parseYMD } from '../../utils/date' +import { createDate, constrainDate, formatYMD, parseYMD } from '../../utils/date' import { isUndefinedOrNull } from '../../utils/inspect' import idMixin from '../../mixins/id' import { BButton } from '../button/button' @@ -31,6 +31,14 @@ const propsMixin = { type: [String, Date], default: '' }, + initialDate: { + // This specifies the calendar year/month/day that will be shown when + // first opening the datepicker if no v-model value is provided + // Default is the current date (or `min`/`max`) + // Passed directly to + type: [String, Date], + default: null + }, placeholder: { type: String, // Defaults to `labelNoDateSelected` from calendar context @@ -241,13 +249,13 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({ return { // We always use `YYYY-MM-DD` value internally localYMD: formatYMD(this.value) || '', + // If the popup is open + isVisible: false, // Context data from BCalendar localLocale: null, isRTL: false, formattedValue: '', - activeYMD: '', - // If the popup is open - isVisible: false + activeYMD: '' } }, computed: { @@ -265,6 +273,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({ value: self.localYMD, min: self.min, max: self.max, + initialDate: self.initialDate, readonly: self.readonly, disabled: self.disabled, locale: self.locale, @@ -293,7 +302,7 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({ return (this.localLocale || '').replace(/-u-.*$/i, '') || null }, computedResetValue() { - return formatYMD(this.resetValue) || '' + return formatYMD(constrainDate(this.resetValue)) || '' } }, watch: { @@ -361,7 +370,8 @@ export const BFormDatepicker = /*#__PURE__*/ Vue.extend({ this.$emit('context', ctx) }, onTodayButton() { - this.setAndClose(formatYMD(createDate())) + // Set to today (or min/max if today is out of range) + this.setAndClose(formatYMD(constrainDate(createDate(), this.min, this.max))) }, onResetButton() { this.setAndClose(this.computedResetValue) diff --git a/src/components/form-datepicker/package.json b/src/components/form-datepicker/package.json index 036cd59b1d9..073a2515dbb 100644 --- a/src/components/form-datepicker/package.json +++ b/src/components/form-datepicker/package.json @@ -28,6 +28,11 @@ "prop": "resetValue", "description": "When the optional `reset` button is clicked, the selected date will be set to this value. Default is to clear the selected value" }, + { + "prop": "initialDate", + "version": "2.7.0", + "description": "When a `value` is not specified, sets the initial calendar month date that will be presented to the user. Accepts a value in `YYYY-MM-DD` format or a `Date` object. Defaults to the current date (or min or max if the current date is out of range)" + }, { "prop": "disabled", "description": "Places the calendar in a non-interactive disabled state" diff --git a/src/components/form-timepicker/package.json b/src/components/form-timepicker/package.json index 08ccf8316d4..92342824416 100644 --- a/src/components/form-timepicker/package.json +++ b/src/components/form-timepicker/package.json @@ -36,7 +36,7 @@ }, { "prop": "showSeconds", - "description": "When true, shows the seconds spinbutton. If `false` the seconds spin button will not be shown and the seconds portion of hte time will always be `0`" + "description": "When true, shows the seconds spinbutton. If `false` the seconds spin button will not be shown and the seconds portion of the time will always be `0`" }, { "prop": "hour12", diff --git a/src/components/time/package.json b/src/components/time/package.json index 7ba9b4a2f24..fbbbfad3950 100644 --- a/src/components/time/package.json +++ b/src/components/time/package.json @@ -17,7 +17,7 @@ }, { "prop": "showSeconds", - "description": "When true, shows the seconds spinbutton. If `false` the seconds spin button will not be shown and the seconds portion of hte time will always be `0`" + "description": "When true, shows the seconds spinbutton. If `false` the seconds spin button will not be shown and the seconds portion of the time will always be `0`" }, { "prop": "hour12", diff --git a/src/utils/date.js b/src/utils/date.js index e6bac5624a0..64774d66e15 100644 --- a/src/utils/date.js +++ b/src/utils/date.js @@ -110,3 +110,14 @@ export const oneYearAhead = date => { } return date } + +// Helper function to constrain a date between two values +// Always returns a `Date` object or `null` if no date passed +export const constrainDate = (date, min = null, max = null) => { + // Ensure values are `Date` objects (or `null`) + date = parseYMD(date) + min = parseYMD(min) || date + max = parseYMD(max) || date + // Return a new `Date` object (or `null`) + return date ? (date < min ? min : date > max ? max : date) : null +} diff --git a/src/utils/date.spec.js b/src/utils/date.spec.js index 31db86c8339..be014e1c406 100644 --- a/src/utils/date.spec.js +++ b/src/utils/date.spec.js @@ -7,7 +7,8 @@ import { oneMonthAgo, oneMonthAhead, oneYearAgo, - oneYearAhead + oneYearAhead, + constrainDate } from './date' describe('utils/date', () => { @@ -94,4 +95,35 @@ describe('utils/date', () => { expect(formatYMD(oneYearAhead(parseYMD('2020-11-30')))).toEqual('2021-11-30') expect(formatYMD(oneYearAhead(parseYMD('2020-12-31')))).toEqual('2021-12-31') }) + + it('costrainDate works', async () => { + const min = parseYMD('2020-01-05') + const max = parseYMD('2020-01-15') + const date1 = parseYMD('2020-01-10') + const date2 = parseYMD('2020-01-01') + const date3 = parseYMD('2020-01-20') + + expect(constrainDate(null, null, null)).toEqual(null) + expect(constrainDate(null, min, max)).toEqual(null) + + expect(constrainDate(date1, null, null)).not.toEqual(null) + expect(constrainDate(date1, null, null).toISOString()).toEqual(date1.toISOString()) + + expect(constrainDate(date1, min, max)).not.toEqual(null) + expect(constrainDate(date1, min, max).toISOString()).toEqual(date1.toISOString()) + + expect(constrainDate(date2, min, max)).not.toEqual(null) + expect(constrainDate(date2, min, max).toISOString()).toEqual(min.toISOString()) + expect(constrainDate(date2, '', max)).not.toEqual(null) + expect(constrainDate(date2, '', max).toISOString()).toEqual(date2.toISOString()) + expect(constrainDate(date2, null, max)).not.toEqual(null) + expect(constrainDate(date2, null, max).toISOString()).toEqual(date2.toISOString()) + + expect(constrainDate(date3, min, max)).not.toEqual(null) + expect(constrainDate(date3, min, max).toISOString()).toEqual(max.toISOString()) + expect(constrainDate(date3, min, '')).not.toEqual(null) + expect(constrainDate(date3, min, '').toISOString()).toEqual(date3.toISOString()) + expect(constrainDate(date3, min, null)).not.toEqual(null) + expect(constrainDate(date3, min, null).toISOString()).toEqual(date3.toISOString()) + }) }) From 13660c3ad02f6c692d306ec95f0d2b19212f9423 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 10 Mar 2020 04:41:50 -0300 Subject: [PATCH 07/29] feat(b-form-datepicker, b-form-timepicker): add support for icon button only mode (closes #4888) (#4915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jacob Müller --- src/components/form-datepicker/README.md | 55 ++++++++++++++++++- .../form-datepicker/form-datepicker.js | 14 ++++- .../form-datepicker/form-datepicker.spec.js | 45 +++++++++++++++ src/components/form-datepicker/package.json | 10 ++++ src/components/form-timepicker/README.md | 54 ++++++++++++++++++ .../form-timepicker/form-timepicker.js | 16 +++++- .../form-timepicker/form-timepicker.spec.js | 45 +++++++++++++++ src/components/form-timepicker/package.json | 10 ++++ src/utils/bv-form-btn-label-control.js | 44 ++++++++++++--- 9 files changed, 283 insertions(+), 10 deletions(-) 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 + + + + + +``` + +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 + + + + + +``` + +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, From 6b172d9e312d11f96d9d5c754870d2cb6be12e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 10 Mar 2020 08:52:32 +0100 Subject: [PATCH 08/29] chore: add avatar image to static assets (#4918) --- static/avatar.png | Bin 0 -> 19951 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/avatar.png diff --git a/static/avatar.png b/static/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..81e71010c0aaeeefaf89cbf3eae1419a275b250e GIT binary patch literal 19951 zcmeFZ`Cm=n7eBtvxkI7o`lP5)CSsl+v7Lm6KkCW+9SX6@^C4gIr@7 zi&QkZR74^aQS{lj_vi8W{tchU&w)xY5Sa z5rBp+X+R^P|Bl3{-~brKev6Zpo(a>)#Aef);2q|1I;$;K8Cf`(hp#u?>0};bW@Nw7 z%tzOhIX^%D>BHCF=Uh(xv%1o@Yxy^GlFJi=^P{}Z1URQXx&LbR&m3#xe{ToI3QpDg zY&|wTHM4U)k$UjT2TT>LhwjpVAGGTEp zOyAvpZC#RGe&P`-i9WvNtWAB$>n&`8(=CG4(oDP$@CGL>PfnCh9c(bFv#(u7*v3oYK z6Sdbf^}M!iPCR6u7GQolWV=nAc~+4AI!orYxb&PRRe&v*H(IWBipD+3Jxe19{XAnqaOTLl zb!>#O6)e#xT&L}rH%DKLkbNLIPB`N_cg5%B$1vO%)^)5QaZ@KQ@zDq>BbI;`dTT03{H?fQ=nn1F%UE<17Wh!4g0L>qf7d zVdXe#oU=a`X8=b9Ab5%xt<4{WR~XG2pOjZ&@$JJ|{5nej;^tS;fHNe>MEzEhL^60( z8X^Y7bNuIXF))SSdn81_8g%%9Bb!260qEAHG%`mN9yR_D1h-OQ^d1>PmO(n$ci4a) zsDIEG17UFb5&9tmj!hFv3?w{yNC5*;AXMNi8JIw11Ei|#;B(`NjRs} zC14*h7Gq+sl+dd|SROx@2I$HR2q6*nd0Ec&(?Ew1NfCCHD5tp*JqS`5pf31moey#` zyG(LtxkPuEF0YW+ku~J{*yam%C z_4re{OCdzxdJbO@GtsqC6E!v?NV5lA2}74`5?O*E=8-TA`^II2>;zH~hmKkX2?D*! zS)qhKIo*t@k)RX9SAHJkJM*qxjM5o^&*W17v$l_HQkdQK_ScZG%1McsuR?u2rk8R= zsX!DHzXCjq?|-cgY<4g28Gq{2+;5#W`9rw|$k5mABY%tT2~a4r1S5Xhd($}pW)uk+ zlS=N6tv03V`#s?ABuT)`&8}GKZf-Go45P!GYm}Qsfb#IaRh!H))zIarooS0N3d3Y0 zeg@3Rt77LpUbaaxIhz3^?(bLrbzBwOfB-%3P(Q#&1teDdLC6dvUmHiVD5S5A z5M*+!QCH=IW_;cHJW9Zb;y@Mx=^%{>Y|{WQ_E|eIHlG@VT#b~Teh%bRG<45Bgv>JL zA|`O6KoB_F(M!AbPb+k<0$9+Dv?!-zo}QS2B-%R5B#F#aKh)D$43kqK0tkBlMs_-_ z2Xtrm6r<1(&Rphv>=7^^#VAn+GBa1FvW;iH11I81n%OHe|^cZaW(h^wW-QN&B zpdqq%Dynn>S=B)rBi36Y=g>LPL<6lti+q559JL#nNM%f#YpB3vtiYzJ6_~7TZb>R| z#tp6&W3)MmUpBxIK;rS*;s_w?@vP-y8Q+4ik4D)-)dP57J0OM=T}-`fWWFUZGI@ZD zJciFE0wcay+n;b~;%6z)Ac*96SDeW~Dq~d%3jjOJ)_xaoKgtXYqq`dc17%DPuoVjz zasO*ws*OE~q>J2sOO_%@g+8wVslwpkn4(LEAI?VTuRVeh$qnkczqVMsVBP*OyFCrZ zn1aN>hOg&RyFzaF&TgmZQ^78Dvkc5BbMMmFhj3_SAI3OZf34XR9YHeWBtG=}lmBq( z-+gRKh|qok_&mJBXc2^zk-3WrAiN$_6P<@E;^Yw+%iL*obQ5}+{-D6a!hd=P+lv=k=oaiM99Pe21tzl;peWxm4Ue+ z^+_{Nb1Z;Ff=b~|1U9v253M)?0>}e7yl1_C?UxVPjXZ2QCIYKpMf~tLAxBMO+!bpR z>`>I+tA?BKu~38u2n6QEMWSK0mXEL#p4UIz(xBfz8s z{w)$DLUDe!@CVf2mT_6;ZvkK=z0MJ3ZY3jF+ui0|1Us>jeaZ}+DV6ejntb;k=xk>5 zy~=T5z$x!xISv<5j~hrRLI8P)B1f>4*`fs4U*Y>e(7gaGc#>8A5~{RDUy*rC<3gJg z$>9hwD}_ZcMpC@tt&sP{OqlulFk*G z3K*g5NW3BG;UdsN@JGiXabUV(71$7ScRz!48!O!bgRblDb4D@vK9(OwO^^Ux$|8ivO*xzV4LLm4o>m+s(SR4W1@E9Cml8W&i_D z7=#=wxZ8?}g)ua0gahBv0B1cuI#IGhBfN=fW8>_@>`+ZZ&pk2-Xe+pTkhJu@Ww=r8 zMirnDCj@uyVw>nP!@`>JZkDh31|+e*{EfGbMPxqU@L>lCGG|<(@c#9Fwg)X-k+jtB zvPx$fjkv#CQRgeXt}4;_m}`kCTP1tptWd=InCA(hivpSX#>{&vIMWIw3hwvUV9lRW z=%Kw!1sbFj-z~=2i{aDrJAwU~gU5s}+P))npT9i}og(lA{uLxLwmU=Om+}%FRgfSv zSGcuYNL2#v=3Xsqa+TDioCr2~U8A)E--BoJGr8B9lDl3N@lajkofILt=q!*A5Kl0~ zDr(S<(R zA%4QD{kthM6%_cxv-4c#Sl&zQ@0*XC;+#aE8sGjE^4H80H7s;Clg()189fKc1j$<) zdsfjaRnmI?GM-PqmeBDh4MJEe+-g2u5qufWu$e%Cd1a?y=>akvf=>oc#rWWH*VSBC zc5{t|3MjlfAr#p9!qVQfS`uq#{A*1vgAA`-$tONWF9^TDWzOBuPPw)Uo7^M-CHoC5lkq`AQjx=#we0=7`X)sox6iI7r2C|x!%C?V zRZXhRK7OOrU)wTkIBYJzujj-nZu z)2HX}B;6Ds1YlAwQ~75ABF6h0YNGoWw0KgNxxXx&eC=nJpe)8#3$}E}@_MV7)1!T$ z9?7t;%?RYsz{)u6Qc%)M@#t9+Gz2cv`_@iP0sDY%6+ZfT1IwGO%;^B)RKIk|pKUld z=NoV@js~9ngAd-sT=+Q($Qmhqkt26Z#EC6{Cp^Up%x~OY4B_|pkF2}olN7G4v87xv zGxjTTN_iQa>AW*-2CHO~AWudZ-YnBSOIixyX+C;uBH@+aE~jG^X?%qL^TkSiAtKP^ zW*%1LyY0jZA^;;>92ZJ9kRTJ~JDymlw^YA21&-^p8EotrJ$`mS87|H{3{pS2htIGG zG+7JrOJc%U&-pCP>D;GWd14Il{GbbJJpgE6Tjr1?&SM_48=k{83-PRAtT=5Kui_?Cj5B?+__3i4r{?bcuP z9~-!I)s;-{Z6SS5q5sXFR>mq_wOen&+Q{)ovT_&JDyo4i(-@3zU5~ERr6Bl+2!Aq5aUWdAaj&icYW?%9-Dj! z*o=n8y&N(pcz5Am-FMekYTWZ6k-2W+$BN1DPi0r#xlk3ZAcOq1dMso;%!%h-zn}Xw z)o&L^iuK=DOQ$h$m`PrD-{;|ll1hKVzU5xQoyFLsp-cVS-3x5K*2#C5j|JEVI|W>E zzkiY++1cY9))l}91{yzaorQa44{17l$J)@1&%q%Sky$G3(YX?leWp-((`}#zL1-D! zx(+_yZT2J@*lut9PVF~P(Y8!pPTD(010?nv#+&vz3{+QAX-^WzLbf0ghqebyUI}z~ z+l(Flp(R%{j4Ipnj=MB>1hiivJ(N%d;^6a3TValB@W{ZuxZv(_44?7{H(CY>r0ze> zVUMYPMk12=VN}}l))E~C{G(aP7nnQj-b3g_+&V_jK)PFeYe(HB%TQEt1DT*N8^7iC{B0q2I8rf%-iB^; zU$`+npeBXpRX4^h!^cFy#7Vx#|E^1TgFzz6;+Rkr8GQU)E_xgjnpKVFoEr;S3#_A` z9-Nu(b#XEp&*fq(#U0+J11I46;#TZGMAWOfu}kD?wjyxvP}{lE1KH3Sg_5(pC4uVg zk8j(;VP#&ir09?paF_R#9SX8c?x3zW3a%m;mx#(%wfm$3hI3!sLPeRtXLQZNNVgqUT~%l8Kg{v2!xgW%jg4 zE2iJ&he>Jna^z(*X~}iIA{jJzqO$8}5H2!@X3CaFqBj9cdo*(h_VWBtBxx~5#Jn^# zAHq|=QrnC6J%5W_*FASda0L{25dn`PERSFq@w@bB4F%;TL>ci)1-SE(T$Sg$Q9Rh- zv1rJG&jwhU>;+;q!a0K2WYqY_nm=3RkE}C)lX|Aq?S{HOBBg!Cq`kf8N;;Rrzh{5g z?)yu73P-Q@pE8kFw2)!k9ZC`Uu>Lv&9IrZRlvngze=hdIM*|AjPAvIsr<%txqaHnQx&hK8dI&3W`;^20q4GO!#U39 z{eN37VtaR8kxrp6-@u2wEU}hMw8=eHGrk<63g>`~WRA9hnbO+YI>lv0oi{#)EjG!M zg)1s1?jn%yUuIaixAeCzc5LQ8=nNj`rEe|0eSIZ9lN-5@5#_RA?&)%#)@aPDvdb<7 zba`o2DZ##aDkpg01bspY6FD)zFC}Z+(=)b-tpq=lRf=cyMmUOq|%7rAa!pdlr57F;0&$N{UbKr z+8s9GXGfk0V`Frmh9z+4hWI-fZra%7Yc+_IQdHt6DxJP(sr)|UaT10486PKTJx*VR ziJW!Q4=dW6vsX*wvm8{MC*4DM|L?M>@ptVaex@p5?;+F<-8h{wP%>+PwUZgdX%AkO zPTPE0#Mq&%D3Bjid#Ri)%3cn;w}1{+Cr7ZEZ2tW1J(xKj?{LX{8Q|5Tjn8j9Y!jmB^C+mg_62_&-qLE0Yn~hU1a54(k#reaN(Y4{o>+ov1 zJ8K9(t&k=%Ajgfzi4^!za(PsgwMBr48U5$%WTXoCwqNr~oZUM)WCllp?SGKWbm>u% z{_Yn;5;3k8fdhoEGAagO|J;u#!`03N9Z0yHSkh^s3U^Nanp5J`tVp>dPXG$5(Uw5I zJoZb3Gig^@xeVZoRq{WSoYT1bA_LZGqqKp=KPU zaajAhG*{YphJ_66k*Pc>2O z=CzL zeFb-rf2FQNxpB``&s|Q@t6^wtqm%D18Yq$SZ<|CcHyMzJ;Be$xSIA68PuxyEJu_*= zd_ltSpp>E=Kfx@u7LqszQ!{i1F@Qp>j2LJr;nDjBWmD+8ntU*5K|nBHm}r*j<`mpr zP9iSCvuPR#&cdSC{WwA{3b6+hc{7mx9>$W2t>-CN$P$x0F<7i-tsMf~U&xSQiVHsG zLS>iy*G$PY1Z)S+JqtEcSf{w6s&Tar>@X=ntTM9-XYal6N?h_CshSBqGA1)m(g;5u zjxtZ_IxJR&la`Q|BBU&UGGZ}ZB{3i~_XJ~WP6+MO^r?by%jC^CL!HdY8qyHAH|RrNqom?b8e~Y`q!F#m+F%8|awVGg;+$QA!Z^!FYHUb883q=G z$D=#rN@PwIK?0U*n0R84B}DtL_>1YBmJMO$;UQ~8OVarvc%o-{E*?@T z*H(IDlR-RjKNCV0P2M^YfNi;Fm`#J__~0^(TbOo=k5jp_bMQQ>+DMK4aV8Uo+@7L) zSDC*aE+ume6TcjV$*s%63inFyW!Oh-afFdb%C&!|VDj0HMax-+T2@L_l*P67J6tv6fCY>B;c;k^Iv%ZPSIawMfTGD4tCWJR-=?bcX$BAG;;4s+kuML|-bue1d%QB{92921yuBWo^k&rMTh6oX7l)Y2OGGmXlU&xI=e<1No5?1MS zk`{1#Lp zcnB^{U-9i`OW;L7wMb)EP4Ma#@v$#RoM!2ZaC zbRzFs&*yPLKD{=G1rwPO2EJxuE2T-C#L&0kclztuiE#v(R>yg^XM4Op_98Svur_6!C8KEnk^ zf~#SgofAjop#?^`vYb|Cgbx%wX=QeQ6$QT=wTg{(`KL)BgcglO#;9cGi2$oFr$1j` zD84Zw^2tpoYtWZ_+TQ5%w6(91ssB+9uKrjpT0-4T1!2&X3&6sMv$4!|!uX&d8s=#C z{!^bwgu^cOiDoL39^p-$q7mGLdDCcW^egGFlT;BxJn>5=xU@csJasHW6&mrg>dY@2 z*`LsOmid=C7w$bZ;h)BHLlUD*74J62D&0@Voyz=Q@d2sg77EVGG%A9v`LCXt(DazT zzZuQj+8+wYh{EQsV<)#&Rz#?D4$6W0&rT^s4#cRhk_}v3lr;c7_A}b#MRNV{;(T`+1usJew3p6;nR#!Ct1%ct!T_UX`DWBe>&>A{E;~Q z7tuP1xM|_N55%EqHL1K*Mf6PR>l+mZG*AZnVxr>nAiuM?U#~WIU)jFUE2Z@ljs0(H zJ6sFp`A;&)hne@pY9%dIKFrT&>t_6oKfl9Gn6-zvTW! zitlCTtl@QF4PLV{z#H0PWagn{|Xp1tF$j!$~%oo-34BSxRCP@_*gEYs6o?BT?Ss|)oh zL-bUGXMePqx<$@2d=`xN#{+lm&DQGnP)|3(Xi?*5eM)F>9kGGuv%Cz{%BZCiR_WQ1 ze9$_3)3d+fxWrex&5yC1YMaoWPrxZmp8nHQ*rf-FU8<`E4EyC_W@0vZJ1NAt^;9|u zweA-P+}T+1+2^5h1jldWY)jBSWR#>sJ@Q7!6)tXC4$4c~&I|Fu*ZHu0S$nRBlR{ow zo~YhIg)e`rhwd4LeLj^A78}wHR|vqIeehn3;}TE(KGIkc&a8nt)V=b!b0w)A?IZZB z<5*g+1(V}Mg{Bg)gC^wwe3VWixh(+#{=PTofIhO-+1QObC8vKNIilTuHZJmB_CEKRTnXvdv zX3N&2C2B7ZB8lxR$mE)ZeeQ6!fXTtF5txulnbPZxE`-eI#Og-EYm6LVnLhmS5d{)vr`z6)-^mkD zx!>dVQH!OMqL6%D3fX*{2RkO@NZO0toR2$-j6BPL^Ox^duT|4AiMzipcM)mrp)V4F z^)0ykxWv}YZ5RUW|2}_O64tU}kSdUGjXKzbN?9}iJ~a6b@9rF?GIU;@P*|=h!n66C z@pnkaA1?nOo@+TOyg>`Q&ntuzu5O+zJuY#P-bp&NA^mAH0_Z`P94|5BXNx*X#kNJh z52B>OX3f9gAj7a04V10jj0&~J2y5YlipSN;>UH!^@oJl@L(4JZ%@_01&EaB+7)-7T zD_sQjUosA6DED}q_9vDjfRarHV1(SKlmw(VB$ zkKHWa{Y?Vrc#K@vu#M=^7_5zVF9N+F;C0%<520I z@x{En{)1ZB6Y6DWVy%0k3dTDAZ|3dpo3_DA+GbzqayBe$?Ep1ZL#!dd1GxdF;IAD&hXc**)PrW(O-uS z{ds#kW6i&*u8v!C)#Z72+rP!w|Di`qs^`Z1rOq`s9Yz={os!l;6!GkR?cgiL4JR@# zw-M4=!wq$E!wq=S3onjZIj5mcwmEBf`$n__sGp30 zQwo#FEw=ja4Ao1O*xIw{4MLSyeVt@~*049CxHncOs{0%j^t1L9!ztnC#<9ec^X;K- z@F-@)_e!n59>lo)lP4~;f4fpAVx8p`>{L#TrrT`UU+)VqTC@%!2#v7Znk`A0Yc@-x z?P{^amhSb6%Jz8>5>uBURdxzt6*~ zW9g`k2yt~E|Iv9&=d4xw-99Dch4Q2Gq>w(#6T^LSE%}xwI!5K;Cl=<|f0)OS|8TBe zm|lgyrjGeB2GOh`-a`Vtak- zS1HFAVzg*_319ohkr^k%5T?Jy2?X)&t0C#8%_`YmyD*A9NrY=PVsu?IEp+6p;3hia zI&R5}hZvzrz}MugJP@L%`xfh)N zjD<-U|DM+)a(&h?yIkhI?TCydQBhMT zImlf~C^B1C!mdD-FooXkJ3Kdd1FV&fdO!L$mO~RxLs(GQI{t~fS4O?A=TSZIxFf<_ zCquK+p_QNGtLd26ZO{pOXIdfr`*I?XsfX5vD{V}(_Cz~6<6Xn;-&*2yiLM6xM-$yh zASe`I47;(@85_TSm=A5LaxFD5XXfi?z1xO0oI$~~AnccF)^H2BKQ1{gaW{0dd}2eo zGliQwWSu_o$fG|)Vkd>$m%rh}d)Lt*4{{{nn?se1RU^EO@p}RGP#%bQbNsZa-UZ9_ z`cWzv_+;&wDJ?*SBpo&%vfh3KPDcnDP`u&_GucH_-O3VfR_a0rnzR;?JH7}g1k(}vBr=S>GK>hx>|oE#b(uY3xJhOhG2f|Awducux4j{?rar=pNmz>+%poPSd{DLrFDjPlc8& zC&%WwLXY=u^DwiOo2em6#<_kTLMYDea5Y(A2yH&TtUU%AkNGMk)m0Vs*e#I{jlL8qXIcmb}Q1P0Qtpe8A^q$FOo6Qf5@)4L!tVu|zrS z2OP!IBtMU(o?Dk#>1xzd7vg9{R@KX<^sF zlU)CTv{%8{Cn>BM@;9UpHmM<>^C=uyjeO4Wb1!ALMXJQbM`ffL8Ds5F>vY}-@A!l^ zRjQqNwiTW&X|uyzs;(1Tu0%BI!8wmJrl2#UAcGCKk}}`74Db_=UA0asG2{qTDTpP0 zVyg!;6pAS<$%BN#_8=ca7leWT5yXue47O4!Ed5{D>hi}0SbGXyS+E>4Dj(qRa*VKK z=&4)Mw)>{VkY)O>Q9GwBYBDx3p+9!)V#k-P_zYVM+y3l4VL9{s+fQk~e zy`B|Gh3!>_8d1XB%_FZ$dv8Y;`Fctbl3%f~9UnbNox*72QRdsA(et($Nv-<4&3GsU z6mZi)#a|I)ZH+gdj2v6!PjAO6rBSdM?a#AJ7xwlBvkZlOU61Oc2Q5#$c5{`5q_Xx< zN7t94VDN~JAx+h3zS*dOQh9}%th8ti49JSn2b<)HK!mT>u_YiAY)oDJ@Yg(s-o-q-3-_B{dd zwB}!bo-@P}shV=cE|gXIp8Jrs$LO5%zv6E?jsazogmyaQky$*qgbQ+5i2Lu0Jcosv z&SINYW>;k(Jd9^3XO^e`s9IQnB(Wn_(z#mjSdz^uc}XI&?}UVHnSvE!W{w3%$Rxj~ z^`HUD@JpcPB}>X!fs%`u8M`oLP|g|e3r^1Rs(|n*Z5?SjLUt&P&OC39o*j^6#&!dF z3Z9eV%=!Pk=3N?J{X4B1hN97mD3cV4eYN$-bl+;AX2=eKHa#wcmiwlzy`0ZiwSSKc1^15{XMy7_Uu!KfCsWJc=_&&iM}4kyq-V z_^lO1mMRsPTVo+iXDX-TRTKP-`QB2PD&%WNX-WA!Zv)~g{OlL&^e((}aZ&@GT)QWb?0>l2!Um2W;#L zFMWE0V3$K-7gBd-?O9#n5%tpY#F|CSeK%>bb7HK8`Q})ugL`7=MBQC>G4h1^EU%l9 z{h>wCh8o~eC#`<;N1r#!sn=s;HM|LwTl4rr&O~ANBN_>sC^yXVDunQXF^|qRQO-E> zGj}lgy1lbaiHl~S2p3U^u0toT47RV5mmt2(qV(d!O@H|Qr3QGZPzhFo(Rfky6=lIk zVSpmk)BGANNg^!06a{M(ULO_Xo$_B*+9EGf0bxk#HG<8=S%g58zbL&vrqI^aw~QAL z?G`aJ1bniFJB1j?H=91T9G7UTUH`vq3I2F5A8RvU^Ab~-dg3?YD>MP0kDw`Fr(wks zYu&Oz6r-yjnlu0HsgArIIVp(Nnar8Fp@Fvw z*-<0_XUv0Qc1+@_H#T6^36{VHQW zjrz8GIK9}=K;6wRfM@MBTbdoP_t5N{hmU{%K2fAMeBjLUALnOid-vkb*lU_5|^KF2rNUr)h*)e3W}B) z5h{g9hxu}lq3kb6nxh-h9d}ZQ?+PQ&BnZXuZ9aG>wmP@Rpl^bY&ICp7tVgfGABmAx z3HWFARe5cFK17~pOjiR-a?+}e&Pk8ag1Pf){fsEL{`ZXbcukl~o?t|I_PJ9m8Dg|w(1bz2$F5von`yco+%Re<7K__M z-Af;gJ3k{^^hdgOMZ&!n7mTXI&$+w?Ojs{8=cOTu5oO>1)}(##^gJK&cBzeG_l~pa zhhOf%xWcV`jazF4pADy>69GB_hn?mRcpNHT`=t>ktqs=(b�Nwh7Yk@3W4L{=SGo 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 literal 0 HcmV?d00001 From 13842fbdb5330a4bc022a63278d89a048bdb4f3f Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Tue, 10 Mar 2020 05:37:26 -0300 Subject: [PATCH 09/29] chore(b-ovelay): fix example --- src/components/overlay/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/overlay/README.md b/src/components/overlay/README.md index 82cac97b67a..f730434477d 100644 --- a/src/components/overlay/README.md +++ b/src/components/overlay/README.md @@ -32,7 +32,7 @@ The overlay visibility is controlled vis the `show` prop. By default the overlay