From 1f59d29d2097f3736ba2901c89aea3340a17b2f7 Mon Sep 17 00:00:00 2001 From: Issayah Date: Mon, 28 Oct 2024 12:38:42 -0500 Subject: [PATCH 1/8] feat!: remove html props -- use equivalent slots fixes #1930 refactor: useTemplateRef --- apps/docs/src/data/components/card.data.ts | 14 -- .../docs/src/data/components/carousel.data.ts | 10 -- apps/docs/src/data/components/form.data.ts | 2 +- .../src/data/components/formCheckbox.data.ts | 1 - .../src/data/components/formRadio.data.ts | 1 - .../src/data/components/formSelect.data.ts | 3 +- .../src/data/components/inputGroup.data.ts | 12 -- apps/docs/src/data/components/popover.data.ts | 4 - .../docs/src/data/components/progress.data.ts | 4 - apps/docs/src/data/components/table.data.ts | 4 - apps/docs/src/data/components/toast.data.ts | 2 +- .../docs/components/demo/CarouselCaptions.vue | 6 +- apps/docs/src/docs/components/progress.md | 8 +- .../src/components/Comps/TFormCheckbox.vue | 1 - .../components/Comps/TFormCheckboxGroup.vue | 1 - .../src/components/Comps/TFormSelect.vue | 1 - .../src/components/BAlert/BAlert.vue | 4 +- .../src/components/BButton/BButton.vue | 4 +- .../src/components/BCard/BCard.vue | 8 +- .../src/components/BCard/BCardFooter.vue | 1 - .../src/components/BCard/BCardHeader.vue | 1 - .../src/components/BCardHeadFoot.vue | 5 +- .../src/components/BCarousel/BCarousel.vue | 8 +- .../components/BCarousel/BCarouselSlide.vue | 14 +- .../src/components/BCollapse/BCollapse.vue | 14 +- .../src/components/BDropdown/BDropdown.vue | 20 ++- .../src/components/BForm/BForm.vue | 4 +- .../src/components/BForm/BFormDatalist.vue | 7 +- .../BFormCheckbox/BFormCheckbox.vue | 4 +- .../BFormCheckbox/BFormCheckboxGroup.vue | 13 +- .../src/components/BFormFile/BFormFile.vue | 4 +- .../src/components/BFormGroup/BFormGroup.vue | 4 +- .../src/components/BFormInput/BFormInput.vue | 17 +-- .../src/components/BFormRadio/BFormRadio.vue | 4 +- .../components/BFormRadio/BFormRadioGroup.vue | 13 +- .../components/BFormSelect/BFormSelect.vue | 12 +- .../BFormSelect/BFormSelectOptionGroup.vue | 7 +- .../BFormSpinbutton/BFormSpinbutton.vue | 4 +- .../src/components/BFormTags/BFormTags.vue | 4 +- .../BFormTextarea/BFormTextarea.vue | 17 +-- .../components/BInputGroup/BInputGroup.vue | 14 +- .../src/components/BModal/BModal.vue | 12 +- .../src/components/BNav/BNavItemDropdown.vue | 4 +- .../src/components/BOffcanvas/BOffcanvas.vue | 6 +- .../src/components/BPopover/BPopover.vue | 34 +---- .../src/components/BProgress/BProgressBar.vue | 17 +-- .../src/components/BTable/BTable.vue | 1 - .../src/components/BTable/BTableLite.vue | 1 - .../src/components/BTabs/BTab.vue | 4 +- .../src/components/BToast/BToast.vue | 4 +- .../src/components/BTooltip/BTooltip.vue | 17 ++- .../src/composables/useFormInput.ts | 6 +- .../src/composables/useFormSelect.ts | 2 - .../src/composables/useTextareaResize.ts | 4 +- .../src/types/ComponentProps.ts | 19 +-- .../src/types/SelectTypes.ts | 1 - .../src/utils/floatingUi.ts | 9 +- .../bootstrap-vue-next/src/utils/sanitizer.ts | 130 ------------------ 58 files changed, 141 insertions(+), 411 deletions(-) delete mode 100644 packages/bootstrap-vue-next/src/utils/sanitizer.ts diff --git a/apps/docs/src/data/components/card.data.ts b/apps/docs/src/data/components/card.data.ts index 14d0451d7..304e85d31 100644 --- a/apps/docs/src/data/components/card.data.ts +++ b/apps/docs/src/data/components/card.data.ts @@ -70,7 +70,6 @@ export default { 'footerBgVariant', 'footerBorderVariant', 'footerClass', - 'footerHtml', 'footerTag', 'footerTextVariant', 'footerVariant', @@ -78,7 +77,6 @@ export default { 'headerBgVariant', 'headerBorderVariant', 'headerClass', - 'headerHtml', 'headerTag', 'headerTextVariant', 'headerVariant', @@ -163,12 +161,6 @@ export default { emits: [], props: { '': { - html: { - type: 'string', - default: undefined, - description: - 'HTML content to place in the card footer, takes precednce over text prop and default slot', - }, text: { type: 'string', default: undefined, @@ -222,12 +214,6 @@ export default { emits: [], props: { '': { - html: { - type: 'string', - default: undefined, - description: - 'HTML content to place in the card header, takes precednce over text prop and default slot', - }, text: { type: 'string', default: undefined, diff --git a/apps/docs/src/data/components/carousel.data.ts b/apps/docs/src/data/components/carousel.data.ts index 99de92cc6..39aee37cf 100644 --- a/apps/docs/src/data/components/carousel.data.ts +++ b/apps/docs/src/data/components/carousel.data.ts @@ -186,11 +186,6 @@ export default { default: undefined, description: 'Text content to place in the caption', }, - captionHtml: { - type: 'string', - default: undefined, - description: 'HTML string content to place in the caption', - }, captionTag: { type: 'string', default: 'h3', @@ -253,11 +248,6 @@ export default { default: undefined, description: 'Text content to place in the text of the slide', }, - textHtml: { - type: 'string', - default: undefined, - description: 'HTML string content to place in the text of the slide', - }, textTag: { type: 'string', default: 'p', diff --git a/apps/docs/src/data/components/form.data.ts b/apps/docs/src/data/components/form.data.ts index 56bed43cc..7b62d2e18 100644 --- a/apps/docs/src/data/components/form.data.ts +++ b/apps/docs/src/data/components/form.data.ts @@ -63,7 +63,7 @@ export default { 'Array of items to render in the component. Note that BFormDatalist only supports Options, not OptionsGroups', }, }), - ['disabledField', 'htmlField', 'id', 'options', 'textField', 'valueField'] + ['disabledField', 'id', 'options', 'textField', 'valueField'] ), } satisfies Record, }, diff --git a/apps/docs/src/data/components/formCheckbox.data.ts b/apps/docs/src/data/components/formCheckbox.data.ts index 464e048cd..69c200b43 100644 --- a/apps/docs/src/data/components/formCheckbox.data.ts +++ b/apps/docs/src/data/components/formCheckbox.data.ts @@ -170,7 +170,6 @@ export default { 'disabled', 'disabledField', 'form', - 'htmlField', 'id', 'name', 'plain', diff --git a/apps/docs/src/data/components/formRadio.data.ts b/apps/docs/src/data/components/formRadio.data.ts index bd2654412..ada9240f0 100644 --- a/apps/docs/src/data/components/formRadio.data.ts +++ b/apps/docs/src/data/components/formRadio.data.ts @@ -126,7 +126,6 @@ export default { 'disabled', 'disabledField', 'form', - 'htmlField', 'id', 'name', 'options', diff --git a/apps/docs/src/data/components/formSelect.data.ts b/apps/docs/src/data/components/formSelect.data.ts index 74909575e..d62c75b54 100644 --- a/apps/docs/src/data/components/formSelect.data.ts +++ b/apps/docs/src/data/components/formSelect.data.ts @@ -45,7 +45,6 @@ export default { 'disabled', 'disabledField', 'form', - 'htmlField', 'id', 'name', 'options', @@ -124,7 +123,7 @@ export default { buildCommonProps({ options: {type: 'readonly (unknown | Record)[]'}, }), - ['disabledField', 'htmlField', 'options', 'textField', 'valueField'] + ['disabledField', 'options', 'textField', 'valueField'] ), } satisfies Record, }, diff --git a/apps/docs/src/data/components/inputGroup.data.ts b/apps/docs/src/data/components/inputGroup.data.ts index 732da3520..dc2869f4b 100644 --- a/apps/docs/src/data/components/inputGroup.data.ts +++ b/apps/docs/src/data/components/inputGroup.data.ts @@ -14,23 +14,11 @@ export default { default: undefined, description: 'Text to append to the input group', }, - appendHtml: { - type: 'string', - default: undefined, - description: - "HTML string to append to the input group. Has precedence over 'append' prop", - }, prepend: { type: 'string', default: undefined, description: 'Text to prepend to the input group', }, - prependHtml: { - type: 'string', - default: undefined, - description: - "HTML string to prepend to the input group. Has precedence over 'prepend' prop", - }, ...pick(buildCommonProps(buildCommonProps()), ['id', 'size', 'tag']), } satisfies Record, }, diff --git a/apps/docs/src/data/components/popover.data.ts b/apps/docs/src/data/components/popover.data.ts index 9329a64da..e91aadef4 100644 --- a/apps/docs/src/data/components/popover.data.ts +++ b/apps/docs/src/data/components/popover.data.ts @@ -36,10 +36,6 @@ export default { type: 'Middleware[]', default: undefined, }, - html: { - type: 'boolean', - default: false, - }, id: { type: 'string', default: undefined, diff --git a/apps/docs/src/data/components/progress.data.ts b/apps/docs/src/data/components/progress.data.ts index 2b7c6ec61..4cf7a8f47 100644 --- a/apps/docs/src/data/components/progress.data.ts +++ b/apps/docs/src/data/components/progress.data.ts @@ -80,10 +80,6 @@ export default { type: 'string', default: undefined, }, - labelHtml: { - type: 'string', - default: undefined, - }, max: { type: 'Numberish', default: undefined, diff --git a/apps/docs/src/data/components/table.data.ts b/apps/docs/src/data/components/table.data.ts index 427b8a7e8..d1c4d3304 100644 --- a/apps/docs/src/data/components/table.data.ts +++ b/apps/docs/src/data/components/table.data.ts @@ -93,10 +93,6 @@ export default { type: 'string', default: undefined, }, - captionHtml: { - type: 'string', - default: undefined, - }, detailsTdClass: { type: 'ClassValue', default: undefined, diff --git a/apps/docs/src/data/components/toast.data.ts b/apps/docs/src/data/components/toast.data.ts index e2bf5fe88..92c0e40c2 100644 --- a/apps/docs/src/data/components/toast.data.ts +++ b/apps/docs/src/data/components/toast.data.ts @@ -100,7 +100,7 @@ export default { default: undefined, }, progressProps: { - type: "Omit", + type: "Omit", default: undefined, description: 'The properties to define the progress bar in the toast. No progress will be shown if left undefined', diff --git a/apps/docs/src/docs/components/demo/CarouselCaptions.vue b/apps/docs/src/docs/components/demo/CarouselCaptions.vue index 0b315a0c8..ded3b58c8 100644 --- a/apps/docs/src/docs/components/demo/CarouselCaptions.vue +++ b/apps/docs/src/docs/components/demo/CarouselCaptions.vue @@ -2,12 +2,8 @@ - - + diff --git a/apps/docs/src/docs/components/progress.md b/apps/docs/src/docs/components/progress.md index 600ababcf..b46292ed7 100644 --- a/apps/docs/src/docs/components/progress.md +++ b/apps/docs/src/docs/components/progress.md @@ -94,7 +94,9 @@ Need more control over the label? Provide your own label by using the default sl
Custom label via property (HTML support)
- + + {{33.333333}} + diff --git a/packages/bootstrap-vue-next/src/components/BModal/BModal.vue b/packages/bootstrap-vue-next/src/components/BModal/BModal.vue index c9b8d3a98..f31edfc62 100644 --- a/packages/bootstrap-vue-next/src/components/BModal/BModal.vue +++ b/packages/bootstrap-vue-next/src/components/BModal/BModal.vue @@ -121,7 +121,7 @@ diff --git a/packages/bootstrap-vue-next/src/composables/useFormInput.ts b/packages/bootstrap-vue-next/src/composables/useFormInput.ts index 0c4092b31..bbe08f2cd 100644 --- a/packages/bootstrap-vue-next/src/composables/useFormInput.ts +++ b/packages/bootstrap-vue-next/src/composables/useFormInput.ts @@ -1,5 +1,5 @@ import type {Numberish} from '../types/CommonTypes' -import {nextTick, onActivated, onMounted, ref, type Ref} from 'vue' +import {nextTick, onActivated, onMounted, ref, type Ref, type ShallowRef} from 'vue' import {useAriaInvalid} from './useAriaInvalid' import {useId} from './useId' import {useDebounceFn, useFocus, useToNumber} from '@vueuse/core' @@ -7,10 +7,12 @@ import type {CommonInputProps} from '../types/FormCommonInputProps' export const useFormInput = ( props: Readonly, + input: + | Readonly> + | Readonly>, modelValue: Ref, modelModifiers: Record<'number' | 'lazy' | 'trim', true | undefined> ) => { - const input = ref(null) const forceUpdateKey = ref(0) const computedId = useId(() => props.id, 'input') diff --git a/packages/bootstrap-vue-next/src/composables/useFormSelect.ts b/packages/bootstrap-vue-next/src/composables/useFormSelect.ts index 2b12d75e3..16e298943 100644 --- a/packages/bootstrap-vue-next/src/composables/useFormSelect.ts +++ b/packages/bootstrap-vue-next/src/composables/useFormSelect.ts @@ -27,7 +27,6 @@ export const useFormSelect = ( const value: unknown = get(option, propsValue.valueField as string) const text: string = get(option, propsValue.textField as string) - const html: string = get(option, propsValue.htmlField as string) const disabled: boolean = get(option, propsValue.disabledField as string) const opts: undefined | unknown[] = propsValue.optionsField @@ -44,7 +43,6 @@ export const useFormSelect = ( return { value, text, - html, disabled, } as SelectOption } diff --git a/packages/bootstrap-vue-next/src/composables/useTextareaResize.ts b/packages/bootstrap-vue-next/src/composables/useTextareaResize.ts index a1c4ddf7c..3177920bb 100644 --- a/packages/bootstrap-vue-next/src/composables/useTextareaResize.ts +++ b/packages/bootstrap-vue-next/src/composables/useTextareaResize.ts @@ -7,14 +7,14 @@ import { nextTick, onMounted, readonly, - type Ref, ref, + type ShallowRef, toRef, } from 'vue' import {isVisible} from '../utils/dom' export const useTextareaResize = ( - input: Ref, + input: Readonly>, props: MaybeRefOrGetter<{ rows: Numberish maxRows: Numberish | undefined diff --git a/packages/bootstrap-vue-next/src/types/ComponentProps.ts b/packages/bootstrap-vue-next/src/types/ComponentProps.ts index 398af8f9c..3b2556e9d 100644 --- a/packages/bootstrap-vue-next/src/types/ComponentProps.ts +++ b/packages/bootstrap-vue-next/src/types/ComponentProps.ts @@ -222,7 +222,6 @@ export interface BFormCheckboxGroupProps { disabled?: boolean disabledField?: string form?: string - htmlField?: string id?: string modelValue?: readonly CheckboxValue[] name?: string @@ -241,7 +240,6 @@ export interface BFormCheckboxGroupProps { export interface BFormDatalistProps { disabledField?: string - htmlField?: string id?: string options?: readonly (unknown | Record)[] textField?: string @@ -309,7 +307,6 @@ export interface BFormRadioGroupProps { disabled?: boolean disabledField?: string form?: string - htmlField?: string id?: string modelValue?: RadioValue name?: string @@ -331,7 +328,6 @@ export interface BFormSelectProps { disabled?: boolean disabledField?: string form?: string - htmlField?: string id?: string labelField?: string modelValue?: SelectValue @@ -355,7 +351,6 @@ export interface BFormSelectOptionProps { export interface BFormSelectOptionGroupProps { disabledField?: string - htmlField?: string label?: string options?: readonly (unknown | Record)[] textField?: string @@ -446,10 +441,8 @@ export interface BFormTextareaProps extends CommonInputProps { export interface BInputGroupProps { append?: string - appendHtml?: string id?: string prepend?: string - prependHtml?: string size?: Size tag?: string } @@ -672,7 +665,7 @@ export interface BPlaceholderWrapperProps { loading?: boolean } -export interface BProgressProps extends Omit { +export interface BProgressProps extends Omit { height?: string } @@ -879,7 +872,6 @@ export interface BCardProps extends ColorExtendables { footerBgVariant?: BgColorVariant | null footerBorderVariant?: BorderColorVariant | null footerClass?: ClassValue - footerHtml?: string footerTag?: string footerTextVariant?: TextColorVariant | null footerVariant?: ColorVariant | null @@ -887,7 +879,6 @@ export interface BCardProps extends ColorExtendables { headerBgVariant?: BgColorVariant | null headerBorderVariant?: BorderColorVariant | null headerClass?: ClassValue - headerHtml?: string headerTag?: string headerTextVariant?: TextColorVariant | null headerVariant?: ColorVariant | null @@ -969,7 +960,6 @@ export interface BCarouselProps { export interface BCarouselSlideProps { background?: string caption?: string - captionHtml?: string captionTag?: string contentTag?: string contentVisibleUp?: string @@ -983,7 +973,6 @@ export interface BCarouselSlideProps { imgWidth?: Numberish interval?: number | 'requestAnimationFrame' text?: string - textHtml?: string textTag?: string } @@ -1043,7 +1032,6 @@ export interface BTableSimpleProps { export interface BTableLiteProps extends BTableSimpleProps { align?: VerticalAlign caption?: string - captionHtml?: string detailsTdClass?: ClassValue fieldColumnClass?: // eslint-disable-next-line @typescript-eslint/no-explicit-any | ((field: TableField) => readonly Record[]) @@ -1156,7 +1144,6 @@ export interface BThProps { export interface BProgressBarProps extends ColorExtendables { animated?: boolean label?: string - labelHtml?: string max?: Numberish precision?: Numberish showProgress?: boolean @@ -1232,7 +1219,7 @@ export interface BToastProps extends ColorExtendables, Omit + progressProps?: Omit showOnPause?: boolean solid?: boolean title?: string @@ -1255,7 +1242,6 @@ export interface BPopoverProps extends TeleporterProps { }> floatingMiddleware?: Middleware[] hideMargin?: number - html?: boolean id?: string inline?: boolean manual?: boolean @@ -1285,7 +1271,6 @@ export interface BTooltipProps extends Omit { export interface BCardHeadFootProps extends ColorExtendables { borderVariant?: BorderColorVariant | null - html?: string tag?: string text?: string } diff --git a/packages/bootstrap-vue-next/src/types/SelectTypes.ts b/packages/bootstrap-vue-next/src/types/SelectTypes.ts index 9a08e33bd..7828cb371 100644 --- a/packages/bootstrap-vue-next/src/types/SelectTypes.ts +++ b/packages/bootstrap-vue-next/src/types/SelectTypes.ts @@ -9,7 +9,6 @@ export type SelectValue = export interface SelectOption { value: T text?: string - html?: string disabled?: boolean } diff --git a/packages/bootstrap-vue-next/src/utils/floatingUi.ts b/packages/bootstrap-vue-next/src/utils/floatingUi.ts index db0f52373..4fc66f4ea 100644 --- a/packages/bootstrap-vue-next/src/utils/floatingUi.ts +++ b/packages/bootstrap-vue-next/src/utils/floatingUi.ts @@ -2,7 +2,6 @@ import type {Boundary, Placement, RootBoundary} from '@floating-ui/vue' export {autoUpdate} from '@floating-ui/vue' import {type DirectiveBinding, h, render} from 'vue' -import {DefaultAllowlist, sanitizeHtml} from './sanitizer' import BPopover from '../components/BPopover/BPopover.vue' import type {BPopoverProps} from '../types/ComponentProps' @@ -38,19 +37,19 @@ export const resolveContent = ( el.setAttribute('data-original-title', title) return { - content: sanitizeHtml(title, DefaultAllowlist), + content: title, } } return {} } if (typeof values === 'string') { return { - content: sanitizeHtml(values, DefaultAllowlist), + content: values, } } return { - title: values?.title ? sanitizeHtml(values?.title, DefaultAllowlist) : undefined, - content: values?.content ? sanitizeHtml(values?.content, DefaultAllowlist) : undefined, + title: values?.title ? values?.title : undefined, + content: values?.content ? values?.content : undefined, } } diff --git a/packages/bootstrap-vue-next/src/utils/sanitizer.ts b/packages/bootstrap-vue-next/src/utils/sanitizer.ts deleted file mode 100644 index 1e9608ab8..000000000 --- a/packages/bootstrap-vue-next/src/utils/sanitizer.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/sanitizer.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - -const uriAttributes = new Set([ - 'background', - 'cite', - 'href', - 'itemtype', - 'longdesc', - 'poster', - 'src', - 'xlink:href', -]) - -const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i - -/** - * A pattern that recognizes a commonly useful subset of URLs that are safe. - * - * Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts - */ -const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i - -/** - * A pattern that matches safe data URLs. Only matches image, video and audio types. - * - * Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts - */ -const DATA_URL_PATTERN = - /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i - -const allowedAttribute = ( - attribute: Readonly, - allowedAttributeList: readonly (string | RegExp)[] -) => { - const attributeName = attribute.nodeName.toLowerCase() - - if (allowedAttributeList.includes(attributeName)) { - if (uriAttributes.has(attributeName)) { - return Boolean( - SAFE_URL_PATTERN.test(attribute.nodeValue || '') || - DATA_URL_PATTERN.test(attribute.nodeValue || '') - ) - } - - return true - } - - // Check if a regular expression validates the attribute. - return allowedAttributeList - .filter((attributeRegex): attributeRegex is RegExp => attributeRegex instanceof RegExp) - .some((regex) => regex.test(attributeName)) -} - -export const DefaultAllowlist = { - // Global attributes allowed on any supplied element below. - '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], - 'a': ['target', 'href', 'title', 'rel'], - 'area': [], - 'b': [], - 'br': [], - 'col': [], - 'code': [], - 'div': [], - 'em': [], - 'hr': [], - 'h1': [], - 'h2': [], - 'h3': [], - 'h4': [], - 'h5': [], - 'h6': [], - 'i': [], - 'img': ['src', 'srcset', 'alt', 'title', 'width', 'height'], - 'li': [], - 'ol': [], - 'p': [], - 'pre': [], - 's': [], - 'small': [], - 'span': [], - 'sub': [], - 'sup': [], - 'strong': [], - 'u': [], - 'ul': [], -} - -export const sanitizeHtml = ( - unsafeHtml: string, - allowList: Readonly>, - sanitizeFunction?: (unsafeHtml: string) => string -) => { - if (!unsafeHtml.length) { - return unsafeHtml - } - - if (sanitizeFunction && typeof sanitizeFunction === 'function') { - return sanitizeFunction(unsafeHtml) - } - - const domParser = new window.DOMParser() - const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html') - const elements: NodeListOf = createdDocument.body.querySelectorAll('*') - - for (const element of elements) { - const elementName = element.nodeName.toLowerCase() - - if (!Object.keys(allowList).includes(elementName)) { - element.remove() - - continue - } - - const attributeList = element.attributes - const allowedAttributes = [...(allowList['*'] || []), ...(allowList[elementName] || [])] - - for (const attribute of attributeList) { - if (!allowedAttribute(attribute, allowedAttributes)) { - element.removeAttribute(attribute.nodeName) - } - } - } - - return createdDocument.body.innerHTML -} From 11ff82e8b6266f81e258b2f67f116c01bd1996c4 Mon Sep 17 00:00:00 2001 From: Issayah Date: Mon, 28 Oct 2024 12:52:40 -0500 Subject: [PATCH 2/8] test: remove unused tests --- .../src/components/BCard/card-footer.spec.ts | 8 -- .../src/components/BCard/card-header.spec.ts | 8 -- .../src/components/BCard/card.spec.ts | 32 ------ .../BInputGroup/input-group.spec.ts | 100 ------------------ .../src/components/card-head-foot.spec.ts | 18 ---- 5 files changed, 166 deletions(-) diff --git a/packages/bootstrap-vue-next/src/components/BCard/card-footer.spec.ts b/packages/bootstrap-vue-next/src/components/BCard/card-footer.spec.ts index 0be01ea77..cabe9a3cd 100644 --- a/packages/bootstrap-vue-next/src/components/BCard/card-footer.spec.ts +++ b/packages/bootstrap-vue-next/src/components/BCard/card-footer.spec.ts @@ -41,14 +41,6 @@ describe('card-header', () => { expect($headfoot.props('borderVariant')).toBe('danger') }) - it('gives prop html to child', () => { - const wrapper = mount(BCardFooter, { - props: {html: 'danger'}, - }) - const $headfoot = wrapper.getComponent(BCardHeadFoot) - expect($headfoot.props('html')).toBe('danger') - }) - it('gives prop tag to child', () => { const wrapper = mount(BCardFooter, { props: {tag: 'span'}, diff --git a/packages/bootstrap-vue-next/src/components/BCard/card-header.spec.ts b/packages/bootstrap-vue-next/src/components/BCard/card-header.spec.ts index b11b82173..b9ac3286c 100644 --- a/packages/bootstrap-vue-next/src/components/BCard/card-header.spec.ts +++ b/packages/bootstrap-vue-next/src/components/BCard/card-header.spec.ts @@ -41,14 +41,6 @@ describe('card-header', () => { expect($headfoot.props('borderVariant')).toBe('danger') }) - it('gives prop html to child', () => { - const wrapper = mount(BCardHeader, { - props: {html: 'danger'}, - }) - const $headfoot = wrapper.getComponent(BCardHeadFoot) - expect($headfoot.props('html')).toBe('danger') - }) - it('gives prop tag to child', () => { const wrapper = mount(BCardHeader, { props: {tag: 'span'}, diff --git a/packages/bootstrap-vue-next/src/components/BCard/card.spec.ts b/packages/bootstrap-vue-next/src/components/BCard/card.spec.ts index e99f28616..24bd39734 100644 --- a/packages/bootstrap-vue-next/src/components/BCard/card.spec.ts +++ b/packages/bootstrap-vue-next/src/components/BCard/card.spec.ts @@ -175,14 +175,6 @@ describe('card', () => { expect($header.exists()).toBe(true) }) - it('has child BCardHeader when prop headerHtml', () => { - const wrapper = mount(BCard, { - props: {headerHtml: 'foobar'}, - }) - const $header = wrapper.findComponent(BCardHeader) - expect($header.exists()).toBe(true) - }) - it('child BCardHeader is given class of headerClass', () => { const wrapper = mount(BCard, { props: {header: 'foobar', headerClass: ['foobar']}, @@ -207,14 +199,6 @@ describe('card', () => { expect($header.props('borderVariant')).toBe('danger') }) - it('child BCardHeader has internal prop html as prop headerHtml', () => { - const wrapper = mount(BCard, { - props: {header: 'foobar', headerHtml: '

foobar

'}, - }) - const $header = wrapper.getComponent(BCardHeader) - expect($header.props('html')).toBe('

foobar

') - }) - it('child BCardHeader has internal prop tag as prop headerTag', () => { const wrapper = mount(BCard, { props: {header: 'foobar', headerTag: 'span'}, @@ -412,14 +396,6 @@ describe('card', () => { expect($footer.exists()).toBe(true) }) - it('has child BCardFooter when prop footerHtml', () => { - const wrapper = mount(BCard, { - props: {footerHtml: '

foobar

'}, - }) - const $footer = wrapper.findComponent(BCardFooter) - expect($footer.exists()).toBe(true) - }) - it('child BCardFooter contains class prop footerClass', () => { const wrapper = mount(BCard, { props: {footer: 'foobar', footerClass: ['foobar']}, @@ -444,14 +420,6 @@ describe('card', () => { expect($footer.props('borderVariant')).toBe('danger') }) - it('child BCardFooter has internal prop html as prop footerHtml', () => { - const wrapper = mount(BCard, { - props: {footerHtml: '

foobarr

'}, - }) - const $footer = wrapper.getComponent(BCardFooter) - expect($footer.props('html')).toBe('

foobarr

') - }) - it('child BCardFooter has internal prop tag as prop footerTag', () => { const wrapper = mount(BCard, { props: {footer: 'foobar', footerTag: 'span'}, diff --git a/packages/bootstrap-vue-next/src/components/BInputGroup/input-group.spec.ts b/packages/bootstrap-vue-next/src/components/BInputGroup/input-group.spec.ts index dffbeee66..5f95ca704 100644 --- a/packages/bootstrap-vue-next/src/components/BInputGroup/input-group.spec.ts +++ b/packages/bootstrap-vue-next/src/components/BInputGroup/input-group.spec.ts @@ -112,52 +112,6 @@ describe('input-group', () => { expect($nested.text()).toBe('foobar') }) - it('has child span element when prop prependHtml', () => { - const wrapper = mount(BInputGroup, { - props: {prependHtml: '

foobar

'}, - }) - const $span = wrapper.find('span') - expect($span.exists()).toBe(true) - }) - - it('has child span has further child span when prop prependHtml', () => { - const wrapper = mount(BInputGroup, { - props: {prependHtml: '

foobar

'}, - }) - const $span = wrapper.get('span') - const $nested = $span.find('span') - expect($nested.exists()).toBe(true) - }) - - it('has child span has further child renders html', () => { - const wrapper = mount(BInputGroup, { - props: {prependHtml: '

foobar

'}, - }) - const $span = wrapper.get('span') - const $nested = $span.get('span') - const $h1 = $nested.find('h1') - expect($h1.exists()).toBe(true) - }) - - it('has child span has further child renders html text', () => { - const wrapper = mount(BInputGroup, { - props: {prependHtml: '

foobar

'}, - }) - const $span = wrapper.get('span') - const $nested = $span.get('span') - const $h1 = $nested.get('h1') - expect($h1.text()).toBe('foobar') - }) - - it('child span prefers to render prop prependHtml over prepend', () => { - const wrapper = mount(BInputGroup, { - props: {prependHtml: '

html

', prepend: 'foobar'}, - }) - const $span = wrapper.get('span') - const $nested = $span.get('span') - expect($nested.text()).toBe('html') - }) - it('does not have child span if prop prepend, but also slot prepend', () => { // May break if a span element is ever non v-if on the element const wrapper = mount(BInputGroup, { @@ -212,52 +166,6 @@ describe('input-group', () => { expect($nested.text()).toBe('foobar') }) - it('has child span element when prop appendHtml', () => { - const wrapper = mount(BInputGroup, { - props: {appendHtml: '

foobar

'}, - }) - const $span = wrapper.find('span') - expect($span.exists()).toBe(true) - }) - - it('has child span has further child span when prop appendHtml', () => { - const wrapper = mount(BInputGroup, { - props: {appendHtml: '

foobar

'}, - }) - const $span = wrapper.get('span') - const $nested = $span.find('span') - expect($nested.exists()).toBe(true) - }) - - it('has child span has further child renders html', () => { - const wrapper = mount(BInputGroup, { - props: {appendHtml: '

foobar

'}, - }) - const $span = wrapper.get('span') - const $nested = $span.get('span') - const $h1 = $nested.find('h1') - expect($h1.exists()).toBe(true) - }) - - it('has child span has further child renders html text', () => { - const wrapper = mount(BInputGroup, { - props: {appendHtml: '

foobar

'}, - }) - const $span = wrapper.get('span') - const $nested = $span.get('span') - const $h1 = $nested.get('h1') - expect($h1.text()).toBe('foobar') - }) - - it('child span prefers to render prop appendHtml over append', () => { - const wrapper = mount(BInputGroup, { - props: {appendHtml: '

html

', append: 'foobar'}, - }) - const $span = wrapper.get('span') - const $nested = $span.get('span') - expect($nested.text()).toBe('html') - }) - it('prop prepend prop append and slot default render in correct order', () => { const wrapper = mount(BInputGroup, { props: {prepend: 'prepend', append: 'append'}, @@ -265,12 +173,4 @@ describe('input-group', () => { }) expect(wrapper.text()).toBe('prependdefaultappend') }) - - it('prop prependHtml prop appendHtml and slot default render in correct order', () => { - const wrapper = mount(BInputGroup, { - props: {prependHtml: 'prepend', appendHtml: 'append'}, - slots: {default: 'default'}, - }) - expect(wrapper.text()).toBe('prependdefaultappend') - }) }) diff --git a/packages/bootstrap-vue-next/src/components/card-head-foot.spec.ts b/packages/bootstrap-vue-next/src/components/card-head-foot.spec.ts index 64920b897..685c7e2d9 100644 --- a/packages/bootstrap-vue-next/src/components/card-head-foot.spec.ts +++ b/packages/bootstrap-vue-next/src/components/card-head-foot.spec.ts @@ -52,16 +52,6 @@ describe('card-head-foot', () => { expect($div.exists()).toBe(true) }) - it('nested div renders html when prop html', () => { - const wrapper = mount(BCardHeadFoot, { - props: {html: 'foobar'}, - }) - const $div = wrapper.get('div') - const $span = $div.find('span') - expect($span.exists()).toBe(true) - expect($span.text()).toBe('foobar') - }) - it('renders default slot', () => { const wrapper = mount(BCardHeadFoot, { slots: {default: 'foobar'}, @@ -83,12 +73,4 @@ describe('card-head-foot', () => { }) expect(wrapper.text()).toBe('slots') }) - - it('renders html over default slot', () => { - const wrapper = mount(BCardHeadFoot, { - slots: {default: 'slots'}, - props: {html: 'foobar'}, - }) - expect(wrapper.text()).toBe('foobar') - }) }) From cf28344e894eae691ab092a5868f12d3cd5bd146 Mon Sep 17 00:00:00 2001 From: Issayah Date: Thu, 31 Oct 2024 10:30:22 -0500 Subject: [PATCH 3/8] feat(BFormGroup): add the ability to automatically get the ids from inputs and apply them to the associated label element --- apps/docs/src/docs/components/form-group.md | 24 +++++++++++++ .../src/components/BFormGroup/BFormGroup.vue | 31 +++++++++++------ .../components/BFormGroup/form-group.spec.ts | 34 +++++++++++++++++++ .../src/composables/useFormInput.ts | 6 +++- packages/bootstrap-vue-next/src/utils/keys.ts | 7 ++++ 5 files changed, 91 insertions(+), 11 deletions(-) diff --git a/apps/docs/src/docs/components/form-group.md b/apps/docs/src/docs/components/form-group.md index ae20333dd..44ed435ca 100644 --- a/apps/docs/src/docs/components/form-group.md +++ b/apps/docs/src/docs/components/form-group.md @@ -78,6 +78,30 @@ You can also apply additional classes to the label via the `label-class` prop, s padding and text alignment utility classes. The `label-class` prop accepts either a string or array of strings. +### Automatic Inheriting of id + +The `BFormGroup` component automatically inherits the id of its child input components, such as BFormInput and BFormTextarea. This functionality ensures that the label element's for attribute is correctly set to match the id of the input component, providing proper association between the label and the input field. + + + + + + + + + ### Horizontal layout By default, the label appears above the input element(s), but you may optionally render horizontal diff --git a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue index ee408873c..240843a54 100644 --- a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue +++ b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue @@ -37,7 +37,7 @@ diff --git a/packages/bootstrap-vue-next/src/components/BFormGroup/form-group.spec.ts b/packages/bootstrap-vue-next/src/components/BFormGroup/form-group.spec.ts index ac10511b5..3d6cd3f61 100644 --- a/packages/bootstrap-vue-next/src/components/BFormGroup/form-group.spec.ts +++ b/packages/bootstrap-vue-next/src/components/BFormGroup/form-group.spec.ts @@ -1,6 +1,9 @@ import {afterEach, describe, expect, it} from 'vitest' import {enableAutoUnmount, mount} from '@vue/test-utils' import BFormGroup from './BFormGroup.vue' +import {h, nextTick} from 'vue' +import BFormInput from '../BFormInput/BFormInput.vue' +import BFormTextarea from '../BFormTextarea/BFormTextarea.vue' describe('form-group', () => { enableAutoUnmount(afterEach) @@ -380,4 +383,35 @@ describe('form-group', () => { }) expect(wrapper.attributes('aria-labelledby')).toBeDefined() }) + + describe('provide functionality', () => { + it('label should automatically inherit input id', async () => { + const wrapper = mount(BFormGroup, { + props: {label: 'foo'}, + slots: { + default: h(BFormInput, {id: 'foobar'}), + }, + }) + await nextTick() + expect(wrapper.get('label').attributes('for')).toBe('foobar') + const textArea = mount(BFormGroup, { + props: {label: 'foo'}, + slots: { + default: h(BFormTextarea, {id: 'foobar'}), + }, + }) + await nextTick() + expect(textArea.get('label').attributes('for')).toBe('foobar') + }) + it('uses prop labelFor over input id', async () => { + const wrapper = mount(BFormGroup, { + props: {label: 'foo', labelFor: 'spam and eggs'}, + slots: { + default: h(BFormInput, {id: 'foobar'}), + }, + }) + await nextTick() + expect(wrapper.get('label').attributes('for')).toBe('spam and eggs') + }) + }) }) diff --git a/packages/bootstrap-vue-next/src/composables/useFormInput.ts b/packages/bootstrap-vue-next/src/composables/useFormInput.ts index bbe08f2cd..c0f9719b4 100644 --- a/packages/bootstrap-vue-next/src/composables/useFormInput.ts +++ b/packages/bootstrap-vue-next/src/composables/useFormInput.ts @@ -1,9 +1,10 @@ import type {Numberish} from '../types/CommonTypes' -import {nextTick, onActivated, onMounted, ref, type Ref, type ShallowRef} from 'vue' +import {inject, nextTick, onActivated, onMounted, ref, type Ref, type ShallowRef} from 'vue' import {useAriaInvalid} from './useAriaInvalid' import {useId} from './useId' import {useDebounceFn, useFocus, useToNumber} from '@vueuse/core' import type {CommonInputProps} from '../types/FormCommonInputProps' +import {formGroupPluginKey} from '../utils/keys' export const useFormInput = ( props: Readonly, @@ -19,6 +20,9 @@ export const useFormInput = ( const debounceNumber = useToNumber(() => props.debounce ?? 0) const debounceMaxWaitNumber = useToNumber(() => props.debounceMaxWait ?? NaN) + // This automatically adds the appropriate "for" attribute to a BFormGroup label + inject(formGroupPluginKey, null)?.(computedId) + const internalUpdateModelValue = useDebounceFn( (value: Numberish) => { modelValue.value = value diff --git a/packages/bootstrap-vue-next/src/utils/keys.ts b/packages/bootstrap-vue-next/src/utils/keys.ts index 76b77a1dc..1ce48070c 100644 --- a/packages/bootstrap-vue-next/src/utils/keys.ts +++ b/packages/bootstrap-vue-next/src/utils/keys.ts @@ -198,3 +198,10 @@ export const popoverPluginKey: InjectionKey<{ setTooltip: (self: ControllerKey, val: Partial) => void removeTooltip: (self: ControllerKey) => void }> = createBvnInjectionKey('popoverPlugin') + +/** + * Automatically use a "for" attribute on label elements for its associated input + * Works on BFormInput & Textarea + */ +export const formGroupPluginKey: InjectionKey<(id: Ref) => void> = + createBvnInjectionKey('formGroupPlugin') From fadbd2a06f61f270da681bc18bfffab4c454612e Mon Sep 17 00:00:00 2001 From: Issayah Date: Mon, 4 Nov 2024 12:09:23 -0600 Subject: [PATCH 4/8] feat(BFormRadioGroup)!: add slot option(${index}) to replace html prop feat(BFormCheckboxGroup)!: add slot option(${index}) to replace html prop --- .../src/data/components/formCheckbox.data.ts | 25 +++++- .../src/data/components/formRadio.data.ts | 23 +++++ .../BFormCheckbox/BFormCheckboxGroup.vue | 19 ++++- .../BFormCheckbox/form-checkbox-group.spec.ts | 83 +++++++++++++++++++ .../components/BFormRadio/BFormRadioGroup.vue | 38 ++++++--- .../BFormRadio/form-radio-group.spec.ts | 75 +++++++++++++++++ 6 files changed, 246 insertions(+), 17 deletions(-) create mode 100644 packages/bootstrap-vue-next/src/components/BFormCheckbox/form-checkbox-group.spec.ts diff --git a/apps/docs/src/data/components/formCheckbox.data.ts b/apps/docs/src/data/components/formCheckbox.data.ts index 69c200b43..a38579f40 100644 --- a/apps/docs/src/data/components/formCheckbox.data.ts +++ b/apps/docs/src/data/components/formCheckbox.data.ts @@ -18,7 +18,7 @@ export default { type: 'boolean', default: false, description: - "When set, renders the checkbox as part of a button group (it doesn't enclose the checkbox and label with a div). It is not necessary to set this to true if this is part of a RadioGroup as it is handled internally", + "When set, renders the checkbox as part of a button group (it doesn't enclose the checkbox and label with a div). It is not necessary to set this to true if this is part of a CheckboxGroup as it is handled internally", }, buttonVariant: { type: 'ButtonVariant | null', @@ -207,6 +207,29 @@ export default { 'Slot to place for checkboxes so that they appear before checks generated from options prop', scope: [], }, + { + // eslint-disable-next-line no-template-curly-in-string + name: 'option(${string})', + description: + 'Use this slot to have finer control over the content render inside each checkbox button. option() applies to all buttons, option(index) goes for a specific checkbox button', + scope: [ + { + prop: 'value', + type: 'string | number | undefined', + description: 'The value of the checkbox button', + }, + { + prop: 'disabled', + type: 'boolean | undefined', + description: 'Whether the checkbox button is disabled', + }, + { + prop: 'text', + type: 'string | undefined', + description: 'The text to display for the checkbox button', + }, + ], + }, ], }, ], diff --git a/apps/docs/src/data/components/formRadio.data.ts b/apps/docs/src/data/components/formRadio.data.ts index ada9240f0..6f1879874 100644 --- a/apps/docs/src/data/components/formRadio.data.ts +++ b/apps/docs/src/data/components/formRadio.data.ts @@ -164,6 +164,29 @@ export default { 'Slot to place for radio buttons so that they appear before radios generated from options prop', scope: [], }, + { + // eslint-disable-next-line no-template-curly-in-string + name: 'option(${string})', + description: + 'Use this slot to have finer control over the content render inside each radio button. option() applies to all buttons, option(index) goes for a specific radio button', + scope: [ + { + prop: 'value', + type: 'string | number | undefined', + description: 'The value of the radio button', + }, + { + prop: 'disabled', + type: 'boolean | undefined', + description: 'Whether the radio button is disabled', + }, + { + prop: 'text', + type: 'string | undefined', + description: 'The text to display for the radio button', + }, + ], + }, ], }, ], diff --git a/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckboxGroup.vue b/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckboxGroup.vue index 623f0c890..e86f40ad5 100644 --- a/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckboxGroup.vue +++ b/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckboxGroup.vue @@ -9,8 +9,15 @@ tabindex="-1" > - - {{ item.text }} + + + {{ item.text }} + @@ -50,11 +57,17 @@ const _props = withDefaults(defineProps) => any // eslint-disable-next-line @typescript-eslint/no-explicit-any first?: (props: Record) => any + [key: `option(${string})`]: (props: { + value: string | number | undefined + disabled: boolean | undefined + text: string | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) => any }>() const modelValue = defineModel>({ diff --git a/packages/bootstrap-vue-next/src/components/BFormCheckbox/form-checkbox-group.spec.ts b/packages/bootstrap-vue-next/src/components/BFormCheckbox/form-checkbox-group.spec.ts new file mode 100644 index 000000000..ea63e44d3 --- /dev/null +++ b/packages/bootstrap-vue-next/src/components/BFormCheckbox/form-checkbox-group.spec.ts @@ -0,0 +1,83 @@ +import {afterEach, describe, expect, it} from 'vitest' +import {enableAutoUnmount, mount} from '@vue/test-utils' +import BFormCheckbox from './BFormCheckbox.vue' +import BFormCheckboxGroup from './BFormCheckboxGroup.vue' + +describe('form-checkbox-group', () => { + enableAutoUnmount(afterEach) + + describe('options prop', () => { + it('renders strings', async () => { + const wrapper = mount(BFormCheckboxGroup, { + props: { + options: ['0', '1', '2'], + }, + }) + const checkboxes = wrapper.findAllComponents(BFormCheckbox) + checkboxes.forEach((element, index) => { + expect(element.text()).toBe(index.toString()) + }) + }) + + it('renders numbers', async () => { + const wrapper = mount(BFormCheckboxGroup, { + props: { + options: [0, 1, 2], + }, + }) + const checkboxes = wrapper.findAllComponents(BFormCheckbox) + checkboxes.forEach((element, index) => { + expect(element.text()).toBe(index.toString()) + }) + }) + + it('renders objects', async () => { + const wrapper = mount(BFormCheckboxGroup, { + props: { + options: [ + { + text: '0', + }, + { + text: '1', + }, + { + text: '2', + }, + ], + }, + }) + const checkboxes = wrapper.findAllComponents(BFormCheckbox) + checkboxes.forEach((element, index) => { + expect(element.text()).toBe(index.toString()) + }) + }) + + it('renders objects that uses slots options()', async () => { + const wrapper = mount(BFormCheckboxGroup, { + props: { + options: [ + { + text: '0', + }, + { + text: '1', + }, + { + text: '2', + }, + ], + }, + slots: { + 'option()': 'foo', + 'option(0)': 'bar', + 'option(2)': 'buzz', + }, + }) + const checkboxes = wrapper.findAllComponents(BFormCheckbox) + expect(checkboxes[0].text()).toBe('bar') + expect(checkboxes[1].text()).toBe('foo') + expect(checkboxes[2].text()).toBe('buzz') + }) + }) +}) diff --git a/packages/bootstrap-vue-next/src/components/BFormRadio/BFormRadioGroup.vue b/packages/bootstrap-vue-next/src/components/BFormRadio/BFormRadioGroup.vue index 05d7983ee..a2526a600 100644 --- a/packages/bootstrap-vue-next/src/components/BFormRadio/BFormRadioGroup.vue +++ b/packages/bootstrap-vue-next/src/components/BFormRadio/BFormRadioGroup.vue @@ -9,13 +9,15 @@ tabindex="-1" > - - {{ item.text }} + + + {{ item.text }} + @@ -54,11 +56,17 @@ const _props = withDefaults(defineProps }) const props = useDefaults(_props, 'BFormRadioGroup') -defineSlots<{ +const slots = defineSlots<{ // eslint-disable-next-line @typescript-eslint/no-explicit-any default?: (props: Record) => any // eslint-disable-next-line @typescript-eslint/no-explicit-any first?: (props: Record) => any + [key: `option(${string})`]: (props: { + value: string | number | undefined + disabled: boolean | undefined + text: string | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) => any }>() const modelValue = defineModel>({ @@ -93,15 +101,19 @@ const normalizeOptions = computed(() => props.options.map((el, ind) => typeof el === 'string' || typeof el === 'number' ? { - value: el, - disabled: props.disabled, + props: { + value: el, + disabled: props.disabled, + }, text: el.toString(), self: Symbol(`radioGroupOptionItem${ind}`), } : { - value: el[props.valueField] as string | undefined, - disabled: el[props.disabledField] as boolean | undefined, - ...(el.props ? el.props : {}), + props: { + value: el[props.valueField] as string | undefined, + disabled: el[props.disabledField] as boolean | undefined, + ...(el.props ? el.props : {}), + }, text: el[props.textField] as string | undefined, self: Symbol(`radioGroupOptionItem${ind}`), } diff --git a/packages/bootstrap-vue-next/src/components/BFormRadio/form-radio-group.spec.ts b/packages/bootstrap-vue-next/src/components/BFormRadio/form-radio-group.spec.ts index 4f24a1ac9..47bf8e9f4 100644 --- a/packages/bootstrap-vue-next/src/components/BFormRadio/form-radio-group.spec.ts +++ b/packages/bootstrap-vue-next/src/components/BFormRadio/form-radio-group.spec.ts @@ -128,4 +128,79 @@ describe('form-radio-group', () => { }) expect(wrapper.attributes('aria-invalid')).toBeUndefined() }) + + describe('options prop', () => { + it('renders strings', async () => { + const wrapper = mount(BFormRadioGroup, { + props: { + options: ['0', '1', '2'], + }, + }) + const radios = wrapper.findAllComponents(BFormRadio) + radios.forEach((element, index) => { + expect(element.text()).toBe(index.toString()) + }) + }) + + it('renders numbers', async () => { + const wrapper = mount(BFormRadioGroup, { + props: { + options: [0, 1, 2], + }, + }) + const radios = wrapper.findAllComponents(BFormRadio) + radios.forEach((element, index) => { + expect(element.text()).toBe(index.toString()) + }) + }) + + it('renders objects', async () => { + const wrapper = mount(BFormRadioGroup, { + props: { + options: [ + { + text: '0', + }, + { + text: '1', + }, + { + text: '2', + }, + ], + }, + }) + const radios = wrapper.findAllComponents(BFormRadio) + radios.forEach((element, index) => { + expect(element.text()).toBe(index.toString()) + }) + }) + + it('renders objects that uses slots options()', async () => { + const wrapper = mount(BFormRadioGroup, { + props: { + options: [ + { + text: '0', + }, + { + text: '1', + }, + { + text: '2', + }, + ], + }, + slots: { + 'option()': 'foo', + 'option(0)': 'bar', + 'option(2)': 'buzz', + }, + }) + const radios = wrapper.findAllComponents(BFormRadio) + expect(radios[0].text()).toBe('bar') + expect(radios[1].text()).toBe('foo') + expect(radios[2].text()).toBe('buzz') + }) + }) }) From b4b1f1f198071f1029427a19c0d45b13a2f8466e Mon Sep 17 00:00:00 2001 From: Issayah Date: Wed, 6 Nov 2024 10:02:48 -0600 Subject: [PATCH 5/8] feat: add option() slot syntax to other group components --- .../src/components/BForm/BFormDatalist.vue | 15 ++++++++++++++- .../src/components/BFormSelect/BFormSelect.vue | 15 ++++++++++++++- .../BFormSelect/BFormSelectOptionGroup.vue | 15 ++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/bootstrap-vue-next/src/components/BForm/BFormDatalist.vue b/packages/bootstrap-vue-next/src/components/BForm/BFormDatalist.vue index c73b7a7e4..01e61376d 100644 --- a/packages/bootstrap-vue-next/src/components/BForm/BFormDatalist.vue +++ b/packages/bootstrap-vue-next/src/components/BForm/BFormDatalist.vue @@ -3,7 +3,14 @@ @@ -35,6 +42,12 @@ defineSlots<{ default?: (props: Record) => any // eslint-disable-next-line @typescript-eslint/no-explicit-any first?: (props: Record) => any + [key: `option(${string})`]: (props: { + value: T + disabled: boolean | undefined + text: string | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) => any }>() const computedId = useId(() => props.id, 'datalist') diff --git a/packages/bootstrap-vue-next/src/components/BFormSelect/BFormSelect.vue b/packages/bootstrap-vue-next/src/components/BFormSelect/BFormSelect.vue index b65c9251a..affb31a0b 100644 --- a/packages/bootstrap-vue-next/src/components/BFormSelect/BFormSelect.vue +++ b/packages/bootstrap-vue-next/src/components/BFormSelect/BFormSelect.vue @@ -24,7 +24,14 @@ :disabled-field="props.disabledField" /> - {{ option.text }} + + {{ option.text }} + @@ -72,6 +79,12 @@ defineSlots<{ default?: (props: Record) => any // eslint-disable-next-line @typescript-eslint/no-explicit-any first?: (props: Record) => any + [key: `option(${string})`]: (props: { + value: T + disabled: boolean | undefined + text: string | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) => any }>() const modelValue = defineModel({ diff --git a/packages/bootstrap-vue-next/src/components/BFormSelect/BFormSelectOptionGroup.vue b/packages/bootstrap-vue-next/src/components/BFormSelect/BFormSelectOptionGroup.vue index 6da069941..f3136a963 100644 --- a/packages/bootstrap-vue-next/src/components/BFormSelect/BFormSelectOptionGroup.vue +++ b/packages/bootstrap-vue-next/src/components/BFormSelect/BFormSelectOptionGroup.vue @@ -8,7 +8,14 @@ :value="option.value" v-bind="$attrs" > - {{ option.text }} + + {{ option.text }} + @@ -36,6 +43,12 @@ defineSlots<{ default?: (props: Record) => any // eslint-disable-next-line @typescript-eslint/no-explicit-any first?: (props: Record) => any + [key: `option(${string})`]: (props: { + value: T + disabled: boolean | undefined + text: string | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) => any }>() const {normalizedOptions} = useFormSelect(() => props.options, props) From de0af1bc4bfd30155c565b5d1fa03675115397ee Mon Sep 17 00:00:00 2001 From: Issayah Date: Fri, 8 Nov 2024 10:16:58 -0600 Subject: [PATCH 6/8] refactor: use defineSlots return value rather than feat(BToast): add defineSlots feat(BNavForm): add props wrapperAttrs & bind attrs.class to li also add prop formClass --- .../src/components/BFormGroup/BFormGroup.vue | 10 ++++----- .../src/components/BNav/BNavForm.vue | 18 +++++++++++++-- .../src/components/BPopover/BPopover.vue | 6 ++--- .../src/components/BTable/BTable.vue | 10 ++++----- .../src/components/BTable/BTableLite.vue | 22 +++++++++---------- .../src/components/BToast/BToast.vue | 11 ++++++++-- .../src/types/ComponentProps.ts | 2 ++ 7 files changed, 51 insertions(+), 28 deletions(-) diff --git a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue index b117ece4b..c247ff8ff 100644 --- a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue +++ b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue @@ -10,7 +10,7 @@ > {{ props.invalidFeedback }} {{ props.validFeedback }} - + {{ props.description }} -