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 16f5d436b..829f3381f 100644 --- a/apps/docs/src/data/components/form.data.ts +++ b/apps/docs/src/data/components/form.data.ts @@ -45,7 +45,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..b7dfb391a 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', @@ -170,7 +170,6 @@ export default { 'disabled', 'disabledField', 'form', - 'htmlField', 'id', 'name', 'plain', @@ -208,6 +207,28 @@ export default { 'Slot to place for checkboxes so that they appear before checks generated from options prop', scope: [], }, + { + name: 'option', + description: + 'Use this slot to have finer control over the content render inside each 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 bd2654412..df0918a55 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', @@ -165,6 +164,28 @@ export default { 'Slot to place for radio buttons so that they appear before radios generated from options prop', scope: [], }, + { + name: 'option', + description: + 'Use this slot to have finer control over the content render inside each 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/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/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/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/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/components/BFormInput/BFormInput.vue b/packages/bootstrap-vue-next/src/components/BFormInput/BFormInput.vue index 05e16b9cd..972bbb64e 100644 --- a/packages/bootstrap-vue-next/src/components/BFormInput/BFormInput.vue +++ b/packages/bootstrap-vue-next/src/components/BFormInput/BFormInput.vue @@ -26,7 +26,7 @@ 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/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/components/BNav/BNavItemDropdown.vue b/packages/bootstrap-vue-next/src/components/BNav/BNavItemDropdown.vue index d9e88de0e..ac4fe4b68 100644 --- a/packages/bootstrap-vue-next/src/components/BNav/BNavItemDropdown.vue +++ b/packages/bootstrap-vue-next/src/components/BNav/BNavItemDropdown.vue @@ -28,7 +28,7 @@ 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') - }) }) diff --git a/packages/bootstrap-vue-next/src/composables/useFormInput.ts b/packages/bootstrap-vue-next/src/composables/useFormInput.ts index 0c4092b31..c0f9719b4 100644 --- a/packages/bootstrap-vue-next/src/composables/useFormInput.ts +++ b/packages/bootstrap-vue-next/src/composables/useFormInput.ts @@ -1,22 +1,28 @@ import type {Numberish} from '../types/CommonTypes' -import {nextTick, onActivated, onMounted, ref, type Ref} 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, + 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') 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/composables/useFormSelect.ts b/packages/bootstrap-vue-next/src/composables/useFormSelect.ts index 2b12d75e3..8fd4c1b69 100644 --- a/packages/bootstrap-vue-next/src/composables/useFormSelect.ts +++ b/packages/bootstrap-vue-next/src/composables/useFormSelect.ts @@ -11,7 +11,7 @@ export const useFormSelect = ( const normalizeOption = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any - option: any + option: unknown ): ComplexSelectOptionRaw | SelectOption => { const propsValue = toValue(props) @@ -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 @@ -42,9 +41,9 @@ export const useFormSelect = ( } return { + ...(typeof option === 'object' ? option : {}), 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..5be984572 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 } @@ -489,6 +482,8 @@ export interface BNavProps { export interface BNavFormProps extends BFormProps { role?: string + wrapperAttrs?: Readonly + formClass?: ClassValue } export interface BNavItemProps extends Omit { @@ -672,7 +667,7 @@ export interface BPlaceholderWrapperProps { loading?: boolean } -export interface BProgressProps extends Omit { +export interface BProgressProps extends Omit { height?: string } @@ -879,7 +874,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 +881,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 +962,6 @@ export interface BCarouselProps { export interface BCarouselSlideProps { background?: string caption?: string - captionHtml?: string captionTag?: string contentTag?: string contentVisibleUp?: string @@ -983,7 +975,6 @@ export interface BCarouselSlideProps { imgWidth?: Numberish interval?: number | 'requestAnimationFrame' text?: string - textHtml?: string textTag?: string } @@ -1043,7 +1034,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 +1146,6 @@ export interface BThProps { export interface BProgressBarProps extends ColorExtendables { animated?: boolean label?: string - labelHtml?: string max?: Numberish precision?: Numberish showProgress?: boolean @@ -1232,7 +1221,7 @@ export interface BToastProps extends ColorExtendables, Omit + progressProps?: Omit showOnPause?: boolean solid?: boolean title?: string @@ -1255,7 +1244,6 @@ export interface BPopoverProps extends TeleporterProps { }> floatingMiddleware?: Middleware[] hideMargin?: number - html?: boolean id?: string inline?: boolean manual?: boolean @@ -1285,7 +1273,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/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') 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 -}