From 06ca0ec3c8efd7939e34c04528805fba6ea8be7b Mon Sep 17 00:00:00 2001 From: Jukka Raimovaara Date: Wed, 27 Mar 2024 13:43:35 +0200 Subject: [PATCH 01/11] vscode vue typescript plugin is now depricated and included in volar --- .vscode/extensions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c0a6e5a48..a7cea0b06 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] + "recommendations": ["Vue.volar"] } From f006662516095584480111a8a7b225fcb5739a8e Mon Sep 17 00:00:00 2001 From: "David W. Gray" Date: Tue, 2 Apr 2024 07:42:50 -0700 Subject: [PATCH 02/11] docs: Add documentation and parity section to contributing.md (#1834) --- CONTRIBUTING.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0562940e..f08445e00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,11 +51,36 @@ This will begin the process to merge your changes into the upstream repository's ## Developing -The project uses a monorepo architecture. The main source files for the package exist in `./packages/bootstrap-vue-next`, this is primarily where developing is done. You can then run `pnpm dev` and it will start all possible development environments. When developing the main package, you will want to open the **bootstrap-vue-next:dev** host. This has hot-reloading to make developing easier. You can use the `./packages/bootstrap-vue-next/src/app.vue` file as a test area for any changes that you make +The project uses a monorepo architecture. The main source files for the package exist in `./packages/bootstrap-vue-next`, this is primarily where developing is done. You can then run `pnpm dev` and it will start all possible development environments. When developing the main package, you will want to open the **bootstrap-vue-next:dev** host. This has hot-reloading to make developing easier. You can use the `./packages/bootstrap-vue-next/src/app.vue` file as a test area for any changes that you make. -You can also make use of the `./apps/playground` directory. The `./apps/playground` directory mimics a user's library and can demonstrate some bugs that may not be visible in the main package. However, it does not contain native hot-reloading and makes for a poor development experience since it requires a built dist copy of the main package (simply run `pnpm build`). The playground is not typically used for development. It is more of a place to view the full behavior of a component +You can also make use of the `./apps/playground` directory. The `./apps/playground` directory mimics a user's library and can demonstrate some bugs that may not be visible in the main package. However, it does not contain native hot-reloading and makes for a poor development experience since it requires a built dist copy of the main package (simply run `pnpm build`). The playground is not typically used for development. It is more of a place to view the full behavior of a component. -You can also use `pnpm dev --filter bootstrap-vue-next` to only open the main host +You can also use `pnpm dev --filter bootstrap-vue-next` to only open the main host. + +## Improving the documentation + +Improving the documentation is a great way to contribute to this project, especially if you're not quite ready to dive into the code. + +We use [vitepress](https://vitepress.dev/) to build our documentation in the `./apps/docs` directory. In order to test the docs, first make sure that you follow the steps in [Setting up your workspace](#setting-up-your-workspace). Then you can run `pnpm dev` from the root and then open the **docs:dev** host. This will hot-reload the documentation to let you easily see your edits. The `*.md` files under `./apps/docs/src/docs` contains the core documentation and the `*.data.ts` files under `./apps/docs/src/data/components` contains JSON files that contains the data to build the component definitions in the documentation. + +## Help Verify BootstrapVue and Bootstrap v5 Parity + +As we close in on a v1 of bootstrap-vue-next, we would like to verify our feature parity with both +[boostrap-vue](https://bootstrap-vue.org/) and [Boottrap v5](https://getbootstrap.com/). + +This verification requires that someone take a close look at the documention for both of the parity +systems and the code and documentation for bootstrap-vue-next to evaluate feature parity. We're using a +spreadsheet to track the fine grained assement, and anyone who consumes bootstrap-vue(-next) should +be able to do the first pass evaluation of a component. + +To contribute: + +- Take a look at the read-only version of the spreadsheet, available [here](https://1drv.ms/x/s!AiUqzkjNYGL6ieBPpQpcR41wo1laZQ). You can filter on `BFormCheckbox` and `BFormCheckboxGroup` in the `Component` column to see an example of components that are being evaludated and just about anything else to see the initial state. +- Read through the `Instructions` tab of the spreadsheet, which provides a suggested process to verify a component. +- Once you're satisfied that you understand the process, request access top the [read/write version of the spreadsheet](https://1drv.ms/x/s!AiUqzkjNYGL6ieBPJZV0b2mgOVgnYw) on the [Bootstrap Vue 3 discord server](https://discord.gg/8VjEkneh). +- Evaluate a component (or two) and get us one step closer to v1! + +More context is available in the comments for [this issue](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/1775). ## Registering New Components From 6c69ff9eea76d6661ca344262231610106170487 Mon Sep 17 00:00:00 2001 From: "David W. Gray" Date: Tue, 2 Apr 2024 07:55:34 -0700 Subject: [PATCH 03/11] feat(BCheckbox)!: Implement reverse and without label (#1825) feat(BFormCheckboxGroup)!: BFormCheckboxGroup no longer emits 'change' and 'input' events, listen for 'update:modelValue' instead feat(BFormCheckbox): Add 'reverse' property to checkbox and checkboxgroup feat(BFormCheckbox): Handle no-label case feat(BRadioGroup)!: BRadioGroup no longer emits 'change' and 'input' events, listen for 'update:modelValue' instead --- .../src/data/components/ComponentReference.ts | 2 +- .../src/data/components/formCheckbox.data.ts | 58 ++++--------------- .../src/data/components/formRadio.data.ts | 50 ++-------------- .../docs/src/docs/components/form-checkbox.md | 23 +++++++- .../BFormCheckbox/BFormCheckbox.vue | 4 ++ .../BFormCheckbox/BFormCheckboxGroup.vue | 14 ++--- .../BFormCheckbox/form-checkbox.spec.ts | 26 ++++++++- .../src/components/BFormRadio/BFormRadio.vue | 4 ++ .../components/BFormRadio/BFormRadioGroup.vue | 14 ++--- .../components/BFormRadio/form-radio.spec.ts | 24 ++++++++ .../src/composables/useFormCheck.ts | 8 ++- packages/bootstrap-vue-next/src/utils/keys.ts | 2 + 12 files changed, 113 insertions(+), 116 deletions(-) diff --git a/apps/docs/src/data/components/ComponentReference.ts b/apps/docs/src/data/components/ComponentReference.ts index 49248d3b8..5f6c07524 100644 --- a/apps/docs/src/data/components/ComponentReference.ts +++ b/apps/docs/src/data/components/ComponentReference.ts @@ -17,7 +17,7 @@ export interface ComponentReference { description?: string }[] slots: { - scope: SlotScopeReference[] + scope?: SlotScopeReference[] name: string description?: string }[] diff --git a/apps/docs/src/data/components/formCheckbox.data.ts b/apps/docs/src/data/components/formCheckbox.data.ts index a459c77e5..5edf6d04d 100644 --- a/apps/docs/src/data/components/formCheckbox.data.ts +++ b/apps/docs/src/data/components/formCheckbox.data.ts @@ -116,6 +116,12 @@ export default { description: 'Controls the validation state appearance of the component. `true` for valid, `false` for invalid, or `null` for no validation state', }, + { + prop: 'reverse', + type: 'boolean', + default: false, + description: 'When set, renders the checkbox or switch on the opposite side', + }, { prop: 'switch', type: 'boolean', @@ -161,30 +167,6 @@ export default { }, ], }, - { - event: 'input', - description: 'Emitted before the checked value is changed', - args: [ - { - arg: 'checked', - type: 'CheckboxValue | readonly CheckboxValue[]', - description: - 'Value of the checkbox before the event. Value will be an array for grouped checkboxes or a single value for standalone checkboxes.', - }, - ], - }, - { - event: 'change', - description: 'Emitted when the checked value is changed', - args: [ - { - arg: 'checked', - type: 'CheckboxValue | readonly CheckboxValue[]', - description: - 'Value of the checkbox. Value will be an array for grouped checkboxes or a single value for standalone checkboxes.', - }, - ], - }, ], slots: [ { @@ -289,6 +271,12 @@ export default { default: false, description: 'Adds the `required` attribute to the form control', }, + { + prop: 'reverse', + type: 'boolean', + default: false, + description: 'When set, renders the checkboxes and switches on the opposite side', + }, { prop: 'size', type: 'Size', @@ -345,28 +333,6 @@ export default { }, ], }, - { - event: 'input', - description: 'Emitted before the selected value(s) are changed', - args: [ - { - arg: 'input', - type: 'CheckboxValue[]', - description: 'Value of the checkboxes before the event. Value will be an array.', - }, - ], - }, - { - event: 'change', - description: 'Emitted when the selected value(s) are changed', - args: [ - { - arg: 'change', - type: 'CheckboxValue[]', - description: 'Value of the checkboxes. Value will be an array.', - }, - ], - }, ], slots: [ { diff --git a/apps/docs/src/data/components/formRadio.data.ts b/apps/docs/src/data/components/formRadio.data.ts index f94061b94..048be1949 100644 --- a/apps/docs/src/data/components/formRadio.data.ts +++ b/apps/docs/src/data/components/formRadio.data.ts @@ -80,6 +80,12 @@ export default { type: 'boolean', default: false, }, + { + prop: 'reverse', + type: 'boolean', + default: false, + description: 'When set, renders the radio button on the opposite side', + }, { prop: 'state', type: 'boolean | null', @@ -92,28 +98,6 @@ export default { }, ], emits: [ - { - event: 'input', - description: '', - args: [ - { - arg: 'input', - description: '', - type: 'boolean | string | unknown[] | Record | number', - }, - ], - }, - { - event: 'change', - description: '', - args: [ - { - arg: 'change', - description: '', - type: 'boolean | string | unknown[] | Record | number', - }, - ], - }, { event: 'update:modelValue', description: '', @@ -239,17 +223,6 @@ export default { }, ], emits: [ - { - args: [ - { - arg: 'input', - description: '', - type: 'unknown', - }, - ], - description: '', - event: 'input', - }, { args: [ { @@ -261,17 +234,6 @@ export default { description: '', event: 'update:modelValue', }, - { - args: [ - { - arg: 'change', - description: '', - type: 'unknown', - }, - ], - description: '', - event: 'change', - }, ], slots: [], }, diff --git a/apps/docs/src/docs/components/form-checkbox.md b/apps/docs/src/docs/components/form-checkbox.md index ab55a00a9..45e435666 100644 --- a/apps/docs/src/docs/components/form-checkbox.md +++ b/apps/docs/src/docs/components/form-checkbox.md @@ -412,6 +412,25 @@ Use the `size` prop to control the size of the checkbox. The default size is med +## Reverse + +Use the `reverse` prop to put your checkboxes and switches on the opposite side of the label. + + + Reverse checkbox + Disabled reverse checkbox + Reverse switch ceckbox input + + + ## Checkbox values and `v-model` By default, `BFormCheckbox` value will be true when checked and false when unchecked. You can customize the checked and unchecked values by specifying the `value` and `unchecked-value` properties, respectively. @@ -934,7 +953,7 @@ const indeterminate = ref(true) v-model:indeterminate="asIndeterminate" aria-describedby="flavors" aria-controls="flavors" - @change="toggleAll" + @update:modelValue="toggleAll" > {{ allSelected ? 'Un-select All' : 'Select All' }} @@ -972,7 +991,7 @@ const indeterminate = ref(true) v-model:indeterminate="asIndeterminate" aria-describedby="flavors" aria-controls="flavors" - @change="toggleAll" + @update:modelValue="toggleAll" > {{ allSelected ? 'Un-select All' : 'Select All' }} diff --git a/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckbox.vue b/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckbox.vue index 0408ff033..c66143f57 100644 --- a/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckbox.vue +++ b/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckbox.vue @@ -54,6 +54,7 @@ const props = withDefaults( name?: string plain?: boolean required?: boolean + reverse?: boolean size?: Size state?: boolean | null switch?: boolean @@ -76,6 +77,7 @@ const props = withDefaults( name: undefined, plain: false, required: undefined, + reverse: false, size: undefined, state: null, switch: false, @@ -138,10 +140,12 @@ const classesObject = computed(() => ({ plain: props.plain || (parentData?.plain.value ?? false), button: props.button || (parentData?.buttons.value ?? false), inline: props.inline || (parentData?.inline.value ?? false), + reverse: props.reverse || (parentData?.reverse.value ?? false), switch: props.switch || (parentData?.switch.value ?? false), state: props.state || parentData?.state.value, size: props.size ?? parentData?.size.value ?? 'md', // This is where the true default is made buttonVariant: props.buttonVariant ?? parentData?.buttonVariant.value ?? 'secondary', // This is where the true default is made + hasDefaultSlot: hasDefaultSlot.value, })) const computedClasses = getClasses(classesObject) const inputClasses = getInputClasses(classesObject) diff --git a/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckboxGroup.vue b/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckboxGroup.vue index e12d78df6..5bd669f35 100644 --- a/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckboxGroup.vue +++ b/packages/bootstrap-vue-next/src/components/BFormCheckbox/BFormCheckboxGroup.vue @@ -21,7 +21,7 @@ + + diff --git a/packages/bootstrap-vue-next/src/components/BFormFile/form-file.spec.ts b/packages/bootstrap-vue-next/src/components/BFormFile/form-file.spec.ts new file mode 100644 index 000000000..6aa4f7b4b --- /dev/null +++ b/packages/bootstrap-vue-next/src/components/BFormFile/form-file.spec.ts @@ -0,0 +1,507 @@ +import { enableAutoUnmount, mount } from '@vue/test-utils' +import { afterEach, describe, expect, it } from 'vitest' +import BFormFile from './BFormFile.vue' + +describe('form-file', () => { + enableAutoUnmount(afterEach) + + it('tag is default div', () => { + const wrapper = mount(BFormFile) + expect(wrapper.element.tagName).toBe('DIV') + }) + + it('has class form-control-{type} when prop size', () => { + const wrapper = mount(BFormFile, { + props: {size: 'lg'}, + }) + + const $input = wrapper.find('input') + expect($input.classes()).toContain('form-control-lg') + }) + + it('does not have class form-control-{type} when prop size undefined', () => { + const wrapper = mount(BFormFile, { + props: {size: undefined}, + }) + + const $input = wrapper.find('input') + expect($input.classes()).not.toContain('form-control-lg') + }) + + it('wrapper has class form-input-file', () => { + const wrapper = mount(BFormFile) + expect(wrapper.find('div').classes()).toContain('form-input-file') + }) + + describe('input attributes', () => { + it('has input element', () => { + const wrapper = mount(BFormFile) + const $input = wrapper.find('input') + expect($input.exists()).toBe(true) + }) + + it('input element has attr id', () => { + const wrapper = mount(BFormFile) + const $input = wrapper.get('input') + expect($input.attributes('id')).toBeDefined() + }) + + it('input element attr id contains content from prop id', () => { + const wrapper = mount(BFormFile, { + props: {id: 'foobar'}, + }) + const $input = wrapper.get('input') + expect($input.attributes('id')).toBe('foobar') + }) + + it('input element has attr type to be file', () => { + const wrapper = mount(BFormFile) + const $input = wrapper.get('input') + expect($input.attributes('type')).toBe('file') + }) + + it('input element has attr disabled when prop disabled', () => { + const wrapper = mount(BFormFile, { + props: {disabled: true}, + }) + const $input = wrapper.get('input') + expect($input.attributes('disabled')).toBe('') + }) + + it('input element does not have attr disabled when prop disabled is false', () => { + const wrapper = mount(BFormFile, { + props: {disabled: false}, + }) + const $input = wrapper.get('input') + expect($input.attributes('disabled')).toBeUndefined() + }) + + it('input element does not have attr disabled when prop disabled is undefined', () => { + const wrapper = mount(BFormFile, { + props: {disabled: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('disabled')).toBeUndefined() + }) + + it('input element has attr required when prop name and prop required', () => { + const wrapper = mount(BFormFile, { + props: {required: true, name: 'foo'}, + }) + const $input = wrapper.get('input') + expect($input.attributes('required')).toBe('') + }) + + it('input element does not have attr required when prop name is empty string and prop required', () => { + const wrapper = mount(BFormFile) + const $input = wrapper.get('input') + expect($input.attributes('required')).toBeUndefined() + }) + + it('input element has attr name to be prop name', () => { + const wrapper = mount(BFormFile, { + props: {name: 'foobar'}, + }) + const $input = wrapper.get('input') + expect($input.attributes('name')).toBe('foobar') + }) + + it('input element has attr name is undefined when prop name undefined', () => { + const wrapper = mount(BFormFile, { + props: {name: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('name')).toBeUndefined() + }) + + it('input element has attr form to be prop form', () => { + const wrapper = mount(BFormFile, { + props: {form: 'foobar'}, + }) + const $input = wrapper.get('input') + expect($input.attributes('form')).toBe('foobar') + }) + + it('input element has attr form is undefined when prop form undefined', () => { + const wrapper = mount(BFormFile, { + props: {form: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('form')).toBeUndefined() + }) + + it('input element has attr aria-label to be prop ariaLabel', () => { + const wrapper = mount(BFormFile, { + props: {ariaLabel: 'foobar'}, + }) + const $input = wrapper.get('input') + expect($input.attributes('aria-label')).toBe('foobar') + }) + + it('input element has attr aria-label is undefined when prop ariaLabel undefined', () => { + const wrapper = mount(BFormFile, { + props: {ariaLabel: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('aria-label')).toBeUndefined() + }) + + it('input element has attr aria-labelledby to be prop ariaLabelledby', () => { + const wrapper = mount(BFormFile, { + props: {ariaLabelledby: 'foobar'}, + }) + const $input = wrapper.get('input') + expect($input.attributes('aria-labelledby')).toBe('foobar') + }) + + it('input element has attr aria-labelledby is undefined when prop ariaLabelledby undefined', () => { + const wrapper = mount(BFormFile, { + props: {ariaLabelledby: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('aria-labelledby')).toBeUndefined() + }) + + it('input element has attr aria-labelledby is undefined when prop ariaLabelledby undefined', () => { + const wrapper = mount(BFormFile, { + props: {ariaLabelledby: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('aria-labelledby')).toBeUndefined() + }) + + it('input element has attr value to be true when value is undefined', () => { + const wrapper = mount(BFormFile, { + props: {value: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('value')).toBeUndefined() + }) + + it('input element aria-required when prop name and prop required true', () => { + const wrapper = mount(BFormFile, { + props: {name: 'foo', required: true}, + }) + const $input = wrapper.get('input') + expect($input.attributes('aria-required')).toBe('true') + }) + + it('input element does not have aria-required when prop name and prop required false', () => { + const wrapper = mount(BFormFile, { + props: {name: 'foo', required: false}, + }) + const $input = wrapper.get('input') + expect($input.attributes('aria-required')).toBeUndefined() + }) + + it('input element has set attr multiple to true when prop multiple is true', () => { + const wrapper = mount(BFormFile, { + props: {multiple: 'true'}, + }) + const $input = wrapper.get('input') + expect($input.attributes('multiple')).toBeDefined() + }) + + it('input element has set attr multiple to false when prop multiple is false', () => { + const wrapper = mount(BFormFile, { + props: {multiple: false}, + }) + const $input = wrapper.get('input') + expect($input.attributes('multiple')).toBeUndefined() + }) + + it('input element has set attr multiple to false when prop multiple is undefined', () => { + const wrapper = mount(BFormFile, { + props: {multiple: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('multiple')).toBeUndefined() + }) + + it('input element has set attr capture to true when prop capture is true', () => { + const wrapper = mount(BFormFile, { + props: {capture: true}, + }) + const $input = wrapper.get('input') + expect($input.attributes('capture')).toBe('true') + }) + + it('input element has set attr capture to false when prop capture is false', () => { + const wrapper = mount(BFormFile, { + props: {capture: false}, + }) + const $input = wrapper.get('input') + expect($input.attributes('capture')).toBe('false') + }) + + it('input element has set attr capture to false when prop disabled is undefined', () => { + const wrapper = mount(BFormFile, { + props: {capture: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('capture')).toBe('false') + }) + + it('input element has set attr accept to empty when prop accept is true', () => { + const wrapper = mount(BFormFile, { + props: {accept: ''}, + }) + const $input = wrapper.get('input') + expect($input.attributes('accept')).toBeUndefined() + }) + + it('input element has set attr accept to "foo" when prop accept is false', () => { + const wrapper = mount(BFormFile, { + props: {accept: 'foo'}, + }) + const $input = wrapper.get('input') + expect($input.attributes('accept')).toBe('foo') + }) + + it('input element does not have attr accept when prop accept is undefined', () => { + const wrapper = mount(BFormFile, { + props: {accept: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('accept')).toBeUndefined() + }) + + it('input element has set attr directory to true when prop directory is true', () => { + const wrapper = mount(BFormFile, { + props: {directory: true}, + }) + const $input = wrapper.get('input') + expect($input.attributes('directory')).toBe('true') + }) + + it('input element has set attr directory to false when prop directory is false', () => { + const wrapper = mount(BFormFile, { + props: {directory: false}, + }) + const $input = wrapper.get('input') + expect($input.attributes('directory')).toBe('false') + }) + + it('input element has set attr directory to false when prop directory is undefined', () => { + const wrapper = mount(BFormFile, { + props: {directory: undefined}, + }) + const $input = wrapper.get('input') + expect($input.attributes('directory')).toBe('false') + }) + + it('default has custom attributes transferred input element', async () => { + const wrapper = mount(BFormFile, { + propsData: { + id: 'foo', + foo: 'bar', + }, + }) + + const $input = wrapper.find('input') + expect($input.attributes('foo')).toBeDefined() + expect($input.attributes('foo')).toEqual('bar') + }) + }) + + describe('label attributes', () => { + it('has label by default', () => { + const wrapper = mount(BFormFile) + const $label = wrapper.find('label') + expect($label.exists()).toBe(true) + }) + + it('has label when has label slot defined', () => { + const wrapper = mount(BFormFile, { + props: {labelClass: 'labelClass'}, + slots: {label: 'foo'}, + }) + const $label = wrapper.get('label') + expect($label).not.toBeUndefined() + expect($label.attributes('class')?.includes('labelClass')) + }) + it('has label has attr for to be defined by default', () => { + const wrapper = mount(BFormFile, { + props: {id: 'fooFile'}, + slots: {default: 'foo'}, + }) + + const $input = wrapper.get('input') + const $label = wrapper.get('label') + + const $labelFor = $label.attributes('for') + const $inputId = $input.attributes('id') + + expect($labelFor).toBe($inputId) + }) + }) + + describe('file button', () => { + it('the button is placed on the start when prop placement is start', () => { + const wrapper = mount(BFormFile, { + props: {id: 'fooFile', placement: 'start'}, + }) + + const $label = wrapper.get('label') + expect($label.classes()).toContain('input-group-text') + + const childElements = wrapper.get('div').findAll('*') + + expect(childElements[0].exists()) + expect(childElements[0].element.tagName).toBe('LABEL') + + expect(childElements[1].exists()) + expect(childElements[1].element.tagName).toBe('INPUT') + }) + it('the button is placed on the end when prop placement is end', () => { + const wrapper = mount(BFormFile, { + props: {id: 'fooFile', placement: 'end'}, + }) + + const $label = wrapper.get('label') + expect($label.classes()).not.toContain('form-label') + expect($label.classes()).toContain('input-group-text') + + const childElements = wrapper.get('div').findAll('*') + + expect(childElements[0].exists()) + expect(childElements[0].element.tagName).toBe('INPUT') + + expect(childElements[1].exists()) + expect(childElements[1].element.tagName).toBe('LABEL') + }) + it('the button is placed on the start when prop placement is undefined', () => { + const wrapper = mount(BFormFile, { + props: {id: 'fooFile'}, + }) + + const $label = wrapper.get('label') + expect($label.classes()).not.toContain('form-label') + expect($label.classes()).toContain('input-group-text') + + const childElements = wrapper.get('div').findAll('*') + + expect(childElements[0].exists()) + expect(childElements[0].element.tagName).toBe('LABEL') + + expect(childElements[1].exists()) + expect(childElements[1].element.tagName).toBe('INPUT') + }) + + it('the button has value text when prop browser text is defined', () => { + const wrapper = mount(BFormFile, { + props: {browserText: 'Browse'}, + }) + + const $label = wrapper.get('label') + expect($label.classes()).not.toContain('form-label') + expect($label.classes()).toContain('input-group-text') + expect($label.text()).toBe('Browse') + }) + it('the button has `Choose` when prop browser text is undefined', () => { + const wrapper = mount(BFormFile) + + const $label = wrapper.get('label') + expect($label.classes()).not.toContain('form-label') + expect($label.classes()).toContain('input-group-text') + expect($label.text()).toBe('Choose') + }) + }) + + describe('model behavior', () => { + it('emits input even when file changed', async () => { + const file = new File(['foo'], 'foo.txt', { + type: 'text/plain', + lastModified: Date.now(), + }) + + const wrapper = mount(BFormFile, {props: {id: 'foo', modelValue: file}}) + + const $input = wrapper.get('input') + await $input.trigger('change') + + expect(wrapper.emitted('change')).toBeDefined() + expect(wrapper.emitted('change')).toHaveLength(1) + expect(wrapper.emitted('change')?.[0][0]).toBeInstanceOf(Event) + }) + + it('emits update:modelValue===file event when file changed', async () => { + const file = new File(['foo'], 'foo.txt', { + type: 'text/plain', + lastModified: Date.now(), + }) + + const dataTransfer = new ClipboardEvent('').clipboardData ?? new DataTransfer() + dataTransfer.items.add(file) + + const wrapper = mount(BFormFile, {props: {id: 'foo', modelValue: undefined}}) + + const $input = wrapper.get('input') + $input.element.files = dataTransfer.files + await $input.trigger('change') + + expect(wrapper.emitted('update:modelValue')).toBeDefined() + expect(wrapper.emitted('update:modelValue')).toHaveLength(1) + expect(wrapper.emitted('update:modelValue')?.[0][0]).toStrictEqual(file) + }) + + it('emits update:modelValue===file event when file changed in multiple mode', async () => { + const file1 = new File(['foo'], 'foo.txt', { + type: 'text/plain', + lastModified: Date.now(), + }) + + const file2 = new File(['foo2'], 'foo2.txt', { + type: 'text/plain', + lastModified: Date.now(), + }) + + const dataTransfer = new ClipboardEvent('').clipboardData ?? new DataTransfer() + dataTransfer.items.add(file1) + dataTransfer.items.add(file2) + + const wrapper = mount(BFormFile, {props: {id: 'foo', modelValue: undefined, multiple: true}}) + + const $input = wrapper.get('input') + $input.element.files = dataTransfer.files + await $input.trigger('change') + + expect(wrapper.emitted('update:modelValue')).toBeDefined() + expect(wrapper.emitted('update:modelValue')).toHaveLength(1) + expect((wrapper.emitted('update:modelValue')?.[0][0] as File[])[0]).toStrictEqual(file1) + expect((wrapper.emitted('update:modelValue')?.[0][0] as File[])[1]).toStrictEqual(file2) + }) + + it('emits update:modelValue===file event when file changed in directory mode', async () => { + const file1 = new File(['foo'], 'foo.txt', { + type: 'text/plain', + lastModified: Date.now(), + }) + + const file2 = new File(['foo2'], 'foo2.txt', { + type: 'text/plain', + lastModified: Date.now(), + }) + + const dataTransfer = new ClipboardEvent('').clipboardData ?? new DataTransfer() + dataTransfer.items.add(file1) + dataTransfer.items.add(file2) + + const wrapper = mount(BFormFile, { + props: {id: 'foo', modelValue: undefined, multiple: true, directory: true}, + }) + + const $input = wrapper.get('input') + $input.element.files = dataTransfer.files + await $input.trigger('change') + + expect(wrapper.emitted('update:modelValue')).toBeDefined() + expect(wrapper.emitted('update:modelValue')).toHaveLength(1) + expect((wrapper.emitted('update:modelValue')?.[0][0] as File[])[0]).toStrictEqual(file1) + expect((wrapper.emitted('update:modelValue')?.[0][0] as File[])[1]).toStrictEqual(file2) + }) + + it('reset() method works in single mode', async () => {}) + it('reset() method works in multiple mode', async () => {}) + }) +}) diff --git a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue index b1983ccac..b99f7134a 100644 --- a/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue +++ b/packages/bootstrap-vue-next/src/components/BFormGroup/BFormGroup.vue @@ -162,7 +162,10 @@ export default defineComponent({ const stateClass = useStateClass(() => props.state) - const computedAriaInvalid = useAriaInvalid(() => props.ariaInvalid, () => props.state) + const computedAriaInvalid = useAriaInvalid( + () => props.ariaInvalid, + () => props.state + ) watch( () => ariaDescribedby, diff --git a/packages/bootstrap-vue-next/src/composables/useModalManager.ts b/packages/bootstrap-vue-next/src/composables/useModalManager.ts index 1bf8bf327..4f4aa664d 100644 --- a/packages/bootstrap-vue-next/src/composables/useModalManager.ts +++ b/packages/bootstrap-vue-next/src/composables/useModalManager.ts @@ -82,8 +82,8 @@ export default (modalOpen: Readonly>) => { ) return { - activePosition: computed(() => - stack?.value.findIndex((el) => el.exposed?.id === currentModal.exposed?.id) + activePosition: computed( + () => stack?.value.findIndex((el) => el.exposed?.id === currentModal.exposed?.id) ), activeModalCount: countStack, } From 259a5af283148a754f71468d35bd9e9c2b196f49 Mon Sep 17 00:00:00 2001 From: James Muriithi <26818458+james-muriithi@users.noreply.github.com> Date: Thu, 4 Apr 2024 18:30:32 +0300 Subject: [PATCH 05/11] fix(BPopover and Btooltip): Fixes bootstrap-vue-next#1232 - do not create a new app fro each tooltip or popover (#1837) fix(BPopover and Btooltip): Fixes bootstrap-vue-next#1232 - do not create a new app fro each tooltip or popover --------- Co-authored-by: xvaara --- .../src/directives/BPopover.ts | 22 ++++------------ .../src/directives/BTooltip.ts | 25 +++++-------------- .../src/utils/floatingUi.ts | 14 ++++------- 3 files changed, 16 insertions(+), 45 deletions(-) diff --git a/packages/bootstrap-vue-next/src/directives/BPopover.ts b/packages/bootstrap-vue-next/src/directives/BPopover.ts index a9e4850ed..420e1ecb4 100644 --- a/packages/bootstrap-vue-next/src/directives/BPopover.ts +++ b/packages/bootstrap-vue-next/src/directives/BPopover.ts @@ -1,4 +1,4 @@ -import {type Directive, ref} from 'vue' +import {type Directive} from 'vue' import { bind, type ElementWithPopper, @@ -16,12 +16,10 @@ export default { const text = resolveContent(binding.value, el) if (!text.content && !text.title) return - - el.$__state = ref({ + bind(el, binding, { ...resolveDirectiveProps(binding, el), ...text, }) - bind(el, binding) }, updated(el, binding) { const isActive = resolveActiveStatus(binding.value) @@ -31,21 +29,11 @@ export default { if (!text.content && !text.title) return - if (!el.$__state) { - // Same binding as above - // This happens when mounting occurs, but binding does not happen ie (if (!text.content && !text.title) return) - // So mounting occurs without a title or content set - el.$__state = ref({ - ...resolveDirectiveProps(binding, el), - ...text, - }) - bind(el, binding) - return - } - el.$__state.value = { + unbind(el) + bind(el, binding, { ...resolveDirectiveProps(binding, el), ...text, - } + }) }, beforeUnmount(el) { unbind(el) diff --git a/packages/bootstrap-vue-next/src/directives/BTooltip.ts b/packages/bootstrap-vue-next/src/directives/BTooltip.ts index c4c86f9fc..bbe8cc73f 100644 --- a/packages/bootstrap-vue-next/src/directives/BTooltip.ts +++ b/packages/bootstrap-vue-next/src/directives/BTooltip.ts @@ -1,4 +1,4 @@ -import {type Directive, ref} from 'vue' +import {type Directive} from 'vue' import { bind, type ElementWithPopper, @@ -16,14 +16,13 @@ export default { const text = resolveContent(binding.value, el) if (!text.content && !text.title) return - - el.$__state = ref({ + + bind(el, binding, { noninteractive: true, ...resolveDirectiveProps(binding, el), title: text.title ?? text.content ?? '', tooltip: isActive, }) - bind(el, binding) }, updated(el, binding) { const isActive = resolveActiveStatus(binding.value) @@ -32,26 +31,14 @@ export default { const text = resolveContent(binding.value, el) if (!text.content && !text.title) return + unbind(el) - if (!el.$__state) { - // Same binding as above - // This happens when mounting occurs, but binding does not happen ie (if (!text.content && !text.title) return) - // So mounting occurs without a title or content set - el.$__state = ref({ - noninteractive: true, - ...resolveDirectiveProps(binding, el), - title: text.title ?? text.content ?? '', - tooltip: isActive, - }) - bind(el, binding) - return - } - el.$__state.value = { + bind(el, binding, { noninteractive: true, ...resolveDirectiveProps(binding, el), title: text.title ?? text.content ?? '', tooltip: isActive, - } + }) }, beforeUnmount(el) { unbind(el) diff --git a/packages/bootstrap-vue-next/src/utils/floatingUi.ts b/packages/bootstrap-vue-next/src/utils/floatingUi.ts index 49c09bc66..5789d3aba 100644 --- a/packages/bootstrap-vue-next/src/utils/floatingUi.ts +++ b/packages/bootstrap-vue-next/src/utils/floatingUi.ts @@ -1,9 +1,10 @@ import type {Placement} from '@floating-ui/vue' export {autoUpdate} from '@floating-ui/vue' -import {type App, createApp, type DirectiveBinding, h, type Ref} from 'vue' +import {type DirectiveBinding, h, render} from 'vue' import {DefaultAllowlist, sanitizeHtml} from './sanitizer' import BPopover from '../components/BPopover.vue' +import type {BPopoverProps} from '../types' // TODO this function doesn't currently resolve with RTL in mind. Once Bootstrap finalizes their RTL, we should make this change here /** @@ -106,26 +107,21 @@ export const resolveDirectiveProps = ( }) export interface ElementWithPopper extends HTMLElement { - $__state?: Ref<{title: string; target: HTMLElement}> - $__app?: App $__element?: HTMLElement } -export const bind = (el: ElementWithPopper, binding: Readonly) => { +export const bind = (el: ElementWithPopper, binding: Readonly, props: BPopoverProps) => { const div = document.createElement('span') if (binding.modifiers.body) document.body.appendChild(div) else if (binding.modifiers.child) el.appendChild(div) else el.parentNode?.insertBefore(div, el.nextSibling) - el.$__app = createApp({render: () => h(BPopover, {...el.$__state?.value})}) - el.$__app.mount(div) + render(h(BPopover, props), div) el.$__element = div } export const unbind = (el: ElementWithPopper) => { const div = el.$__element - el.$__app?.unmount() - delete el.$__app - delete el.$__state + if (div) render(null, div) setTimeout(() => { div?.remove() }, 0) From 6ea759874fd41d3da08ae3cafee052e19b3e51e1 Mon Sep 17 00:00:00 2001 From: Jukka Raimovaara Date: Sun, 14 Apr 2024 03:08:15 +0300 Subject: [PATCH 06/11] fix(BFormSelect): array of numbers or dates (#1839) --- .../bootstrap-vue-next/src/composables/useFormSelect.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/bootstrap-vue-next/src/composables/useFormSelect.ts b/packages/bootstrap-vue-next/src/composables/useFormSelect.ts index 672bd3a3f..fc920a60c 100644 --- a/packages/bootstrap-vue-next/src/composables/useFormSelect.ts +++ b/packages/bootstrap-vue-next/src/composables/useFormSelect.ts @@ -13,6 +13,12 @@ export default (options: MaybeRefOrGetter, props: Record) => { if (typeof option === 'string') { return {value: option, text: option} } + if (typeof option === 'number') { + return {value: option, text: `${option}`} + } + if (option instanceof Date) { + return {value: option, text: option.toLocaleString()} + } const value: unknown = get(option, props.valueField as string) const text: string = get(option, props.textField as string) From de60ce3cd2bc3ee8768331ae8ce69b2c5f3eed73 Mon Sep 17 00:00:00 2001 From: Issayah Date: Sun, 14 Apr 2024 15:20:20 -0500 Subject: [PATCH 07/11] BTable multisort (#1842) feat(BDropdown)!: replace "container" prop with "teleportTo" feat(BDropdown): add teleportDisabled prop feat(BPopover)!: replace "container" prop with "teleportTo" feat(BPopover): add teleportDisabled prop feat: export component prop types refactor: use defineModel > useVModel fixes #1799 fix(generics): use generic constraints for BTable & BTableLite refactor: move all props into ComponentProps.ts -> export types & for future global options overhaul chore: remove dead code chore: remove deprecated vscode extension recommendation fix(BTable): stickyHeader true causes maxHeight 300px feat(BTable): allow Numberish values => string is interpreted as is with maxHeight, numbers are converted to ${number}px maxHeight refactor(BTable): move tableAttrs to a computed for shared props refactor: make useAvatarSize more generic, reuse in BTableSimple feat(BTable)!: deprecate noSortReset, use `mustSort` fix(BTable): fix handleFieldSorting algorithm to properly handle multisort with mustSort feat(BTable): add functional syntax for sortAsc/sortDesc/sortDefault `sortAsc(fieldKey)` to specify the sort content for that specific field fix(BTable): set sort values to undefined so we dont accidentally wipe user defined comparer functions fix(BTable): generic types chore: lint fix: types generation -- use interfaces for componentprops test(BTable): Get unit tests working test(BTableSimple): Update test chore: address type errors in playground test(BFormCheckbox): remove tests for deprecated events docs(BTable): Document sort and add 'complete example' feat(BFormTags)!: remove input event -- use @update:modelValue chore: pin happy-dom to stop failing test issue chore: update deps test: fix some tests fix: some migration issues with defineModel --- .../docs/.vitepress/{config.ts => config.mts} | 0 apps/docs/.vitepress/theme/Layout.vue | 4 +- apps/docs/package.json | 24 +- apps/docs/src/data/components/popover.data.ts | 2 +- apps/docs/src/data/components/table.data.ts | 37 +- apps/docs/src/docs/components/form-tags.md | 4 +- apps/docs/src/docs/components/table.md | 815 ++- apps/playground/package.json | 20 +- .../src/components/Comps/TBLink.vue | 36 +- apps/playground/src/components/Comps/TImg.vue | 4 - .../src/components/Comps/TPopover.vue | 13 +- .../src/components/Comps/TScrollspy.vue | 2 +- .../src/components/Comps/TTable.vue | 100 +- .../playground/src/components/Comps/TTabs.vue | 11 +- apps/playground/tsconfig.app.json | 4 +- apps/playground/tsconfig.node.json | 16 +- package.json | 2 +- packages/bootstrap-vue-next/package.json | 46 +- .../src/components/BAccordion/BAccordion.vue | 29 +- .../components/BAccordion/BAccordionItem.vue | 64 +- .../BAccordion/accordion-item.spec.ts | 7 +- .../src/components/BAlert/BAlert.vue | 53 +- .../src/components/BAvatar/BAvatar.vue | 143 +- .../src/components/BAvatar/BAvatarGroup.vue | 59 +- .../src/components/BBadge/BBadge.vue | 81 +- .../components/BBreadcrumb/BBreadcrumb.vue | 9 +- .../BBreadcrumb/BBreadcrumbItem.vue | 66 +- .../src/components/BButton/BButton.vue | 93 +- .../src/components/BButton/BButtonGroup.vue | 22 +- .../src/components/BButton/BButtonToolbar.vue | 19 +- .../src/components/BButton/BCloseButton.vue | 19 +- .../src/components/BCard/BCard.vue | 146 +- .../src/components/BCard/BCardBody.vue | 46 +- .../src/components/BCard/BCardGroup.vue | 18 +- .../src/components/BCard/BCardImg.vue | 62 +- .../src/components/BCard/BCardSubtitle.vue | 19 +- .../src/components/BCard/BCardText.vue | 16 +- .../src/components/BCard/BCardTitle.vue | 16 +- .../src/components/BCarousel/BCarousel.vue | 84 +- .../components/BCarousel/BCarouselSlide.vue | 64 +- .../src/components/BCollapse.vue | 40 +- .../src/components/BContainer.vue | 22 +- .../src/components/BDropdown/BDropdown.vue | 20 +- .../components/BDropdown/BDropdownDivider.vue | 13 +- .../components/BDropdown/BDropdownGroup.vue | 28 +- .../components/BDropdown/BDropdownItem.vue | 64 +- .../BDropdown/BDropdownItemButton.vue | 25 +- .../components/BDropdown/BDropdownText.vue | 13 +- .../src/components/BDropdown/dropdown.spec.ts | 4 +- .../components/BForm/BFormFloatingLabel.vue | 19 +- .../src/components/BForm/BFormRow.vue | 13 +- .../src/components/BForm/BFormText.vue | 25 +- .../BFormCheckbox/BFormCheckbox.vue | 86 +- .../BFormCheckbox/BFormCheckboxGroup.vue | 85 +- .../BFormCheckbox/form-checkbox.spec.ts | 107 +- .../src/components/BFormFile/BFormFile.vue | 84 +- .../components/BFormFile/form-file.spec.ts | 4 +- .../src/components/BFormInput/BFormInput.vue | 73 +- .../src/components/BFormRadio/BFormRadio.vue | 73 +- .../components/BFormRadio/BFormRadioGroup.vue | 82 +- .../components/BFormSelect/BFormSelect.vue | 92 +- .../BFormSelect/BFormSelectOption.vue | 16 +- .../BFormSelect/BFormSelectOptionGroup.vue | 30 +- .../BFormSpinbutton/BFormSpinbutton.vue | 96 +- .../src/components/BFormTags/BFormTag.vue | 34 +- .../src/components/BFormTags/BFormTags.vue | 119 +- .../BFormTextarea/BFormTextarea.vue | 69 +- .../components/BInputGroup/BInputGroup.vue | 31 +- .../BInputGroup/BInputGroupText.vue | 16 +- .../src/components/BListGroup/BListGroup.vue | 22 +- .../components/BListGroup/BListGroupItem.vue | 67 +- .../src/components/BModal/BModal.vue | 6 +- .../components/BModal/BModalOrchestrator.vue | 16 +- .../src/components/BNav/BNav.vue | 40 +- .../src/components/BNav/BNavForm.vue | 27 +- .../src/components/BNav/BNavItem.vue | 62 +- .../src/components/BNav/BNavItemDropdown.vue | 9 +- .../src/components/BNav/BNavText.vue | 9 +- .../components/BNav/nav-item-dropdown.spec.ts | 27 - .../src/components/BNavbar/BNavbar.vue | 34 +- .../src/components/BNavbar/BNavbarBrand.vue | 59 +- .../src/components/BNavbar/BNavbarNav.vue | 25 +- .../src/components/BNavbar/BNavbarToggle.vue | 18 +- .../src/components/BOffcanvas/BOffcanvas.vue | 95 +- .../src/components/BOverlay/BOverlay.vue | 74 +- .../components/BPagination/BPagination.vue | 104 +- .../components/BPlaceholder/BPlaceholder.vue | 31 +- .../BPlaceholder/BPlaceholderButton.vue | 25 +- .../BPlaceholder/BPlaceholderCard.vue | 67 +- .../BPlaceholder/BPlaceholderTable.vue | 64 +- .../BPlaceholder/BPlaceholderWrapper.vue | 13 +- .../src/components/BPopover.vue | 28 +- .../src/components/BProgress/BProgress.vue | 39 +- .../src/components/BSpinner.vue | 28 +- .../src/components/BTable/BTable.vue | 577 +- .../src/components/BTable/BTableLite.vue | 122 +- .../src/components/BTable/BTableSimple.vue | 32 +- .../src/components/BTable/BTbody.vue | 13 +- .../src/components/BTable/BTd.vue | 25 +- .../src/components/BTable/BTfoot.vue | 13 +- .../src/components/BTable/BTh.vue | 25 +- .../src/components/BTable/BThead.vue | 13 +- .../src/components/BTable/BTr.vue | 13 +- .../src/components/BTable/table-lite.spec.ts | 175 + .../components/BTable/table-simple.spec.ts | 58 + .../src/components/BTable/table.spec.ts | 89 +- .../src/components/BTabs/BTab.vue | 57 +- .../src/components/BTabs/BTabs.vue | 95 +- .../src/components/BToast/BToast.vue | 6 +- .../components/BToast/BToastOrchestrator.vue | 20 +- .../src/components/BTooltip.vue | 8 +- .../components/BTransition/transition.spec.ts | 10 +- .../src/composables/index.ts | 2 +- .../src/composables/useAvatarSize.ts | 10 - .../src/composables/useModalManager.ts | 4 +- .../src/composables/useNumberishToStyle.ts | 21 + .../src/directives/BTooltip.ts | 2 +- .../src/types/CommonTypes.ts | 7 + .../src/types/ComponentProps.ts | 971 +++- .../src/types/LiteralUnion.ts | 18 + .../src/types/TableTypes.ts | 63 +- .../src/types/exports/index.ts | 3 + .../src/utils/floatingUi.ts | 6 +- .../src/utils/formatItem.ts | 9 +- .../src/utils/getTableFieldHeadLabel.ts | 2 +- packages/bootstrap-vue-next/tsconfig.app.json | 2 + .../bootstrap-vue-next/tsconfig.node.json | 14 +- .../bootstrap-vue-next/tsconfig.vitest.json | 2 + packages/nuxt/package.json | 20 +- packages/nuxt/playground/package.json | 2 +- pnpm-lock.yaml | 4953 +++++++++++------ 131 files changed, 7211 insertions(+), 4898 deletions(-) rename apps/docs/.vitepress/{config.ts => config.mts} (100%) create mode 100644 packages/bootstrap-vue-next/src/components/BTable/table-lite.spec.ts create mode 100644 packages/bootstrap-vue-next/src/components/BTable/table-simple.spec.ts delete mode 100644 packages/bootstrap-vue-next/src/composables/useAvatarSize.ts create mode 100644 packages/bootstrap-vue-next/src/composables/useNumberishToStyle.ts diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.mts similarity index 100% rename from apps/docs/.vitepress/config.ts rename to apps/docs/.vitepress/config.mts diff --git a/apps/docs/.vitepress/theme/Layout.vue b/apps/docs/.vitepress/theme/Layout.vue index e34fb4e00..7cdc29206 100644 --- a/apps/docs/.vitepress/theme/Layout.vue +++ b/apps/docs/.vitepress/theme/Layout.vue @@ -89,7 +89,7 @@ | HTMLElement | undefined', default: undefined, }, diff --git a/apps/docs/src/data/components/table.data.ts b/apps/docs/src/data/components/table.data.ts index faf4bb411..0b74cac4c 100644 --- a/apps/docs/src/data/components/table.data.ts +++ b/apps/docs/src/data/components/table.data.ts @@ -43,8 +43,9 @@ export default { }, { prop: 'sortBy', - type: 'string', + type: 'BTableSortBy[]', default: undefined, + description: 'Model representing the current sort state', }, { prop: 'sortDesc', @@ -259,40 +260,26 @@ export default { { args: [ { - arg: 'update:sortBy', - description: '', + arg: 'value', + description: 'BTableSortBy[] | undefined', type: 'string', }, ], event: 'update:sortBy', - description: '', - }, - { - args: [ - { - arg: 'update:sortDesc', - description: 'boolean', - type: 'boolean', - }, - ], - event: 'update:sortDesc', - description: '', + description: + 'Emitted when the `sortBy` model is changed and represents the current sort state', }, { args: [ { - arg: 'sortBy', + arg: 'value', description: '', - type: 'string', - }, - { - arg: 'isDesc', - description: '', - type: 'boolean', + type: 'BTableSortBy', }, ], event: 'sorted', - description: '', + description: + 'Updated when the user clicks a sortable column heading and represents the column click and the sort state (`asc`, `desc`, or undefined)', }, ], slots: [ @@ -485,8 +472,10 @@ export default { }, { prop: 'stickyHeader', - type: 'boolean', + type: 'boolean | Numberish', default: false, + description: + 'Makes the table header sticky. Set to true for a maximum height 300px tall table, or set to any valid CSS height (including units). Inputting a number type is converted to px height', }, ], emits: [], diff --git a/apps/docs/src/docs/components/form-tags.md b/apps/docs/src/docs/components/form-tags.md index 03e5bd0f7..96730e6c2 100644 --- a/apps/docs/src/docs/components/form-tags.md +++ b/apps/docs/src/docs/components/form-tags.md @@ -1231,7 +1231,7 @@ You can easily create a custom wrapper component with your preferred rendering s ```vue + + diff --git a/apps/playground/src/components/Comps/TImg.vue b/apps/playground/src/components/Comps/TImg.vue index 6fc3aff1f..ba6715fc0 100644 --- a/apps/playground/src/components/Comps/TImg.vue +++ b/apps/playground/src/components/Comps/TImg.vue @@ -50,14 +50,12 @@ @@ -65,14 +63,12 @@ diff --git a/apps/playground/src/components/Comps/TPopover.vue b/apps/playground/src/components/Comps/TPopover.vue index bf40072eb..6d878f7e7 100644 --- a/apps/playground/src/components/Comps/TPopover.vue +++ b/apps/playground/src/components/Comps/TPopover.vue @@ -150,14 +150,7 @@ - + @@ -206,7 +199,7 @@ - + -