From f68f840b553c805c71c445e585f5884f69aab97a Mon Sep 17 00:00:00 2001 From: haoziqaq <357229046@qq.com> Date: Sun, 29 Dec 2024 21:49:07 +0800 Subject: [PATCH] feat(menu-select): support cascade mode (#1840) --- .../src/checkbox-group/CheckboxGroup.vue | 8 - .../__snapshots__/index.spec.js.snap | 18 +- .../checkbox-group/__tests__/index.spec.js | 8 +- packages/varlet-ui/src/checkbox/Checkbox.vue | 40 +-- packages/varlet-ui/src/checkbox/checkbox.less | 21 -- packages/varlet-ui/src/checkbox/docs/en-US.md | 2 +- packages/varlet-ui/src/checkbox/docs/zh-CN.md | 2 +- packages/varlet-ui/src/checkbox/props.ts | 2 +- packages/varlet-ui/src/checkbox/provide.ts | 1 - .../varlet-ui/src/menu-option/MenuOption.vue | 84 +++++-- .../varlet-ui/src/menu-option/menuOption.less | 14 ++ packages/varlet-ui/src/menu-option/props.ts | 9 +- .../src/menu-select/MenuChildren.vue | 175 ++++++++++++++ .../varlet-ui/src/menu-select/MenuSelect.vue | 209 +++++++++++++++- .../__snapshots__/index.spec.js.snap | 30 ++- .../src/menu-select/__tests__/index.spec.js | 227 ++++++++++++++++++ .../varlet-ui/src/menu-select/docs/en-US.md | 131 ++++++++++ .../varlet-ui/src/menu-select/docs/zh-CN.md | 131 ++++++++++ .../src/menu-select/example/index.vue | 66 +++++ .../src/menu-select/example/locale/en-US.ts | 3 + .../src/menu-select/example/locale/zh-CN.ts | 3 + .../varlet-ui/src/menu-select/menuSelect.less | 5 + packages/varlet-ui/src/menu-select/props.ts | 10 + packages/varlet-ui/src/menu/Menu.vue | 6 + packages/varlet-ui/src/menu/menu.less | 2 +- packages/varlet-ui/src/menu/props.ts | 4 + packages/varlet-ui/src/menu/usePopover.ts | 15 ++ packages/varlet-ui/src/option/Option.vue | 15 +- packages/varlet-ui/src/option/docs/en-US.md | 1 + packages/varlet-ui/src/option/docs/zh-CN.md | 1 + packages/varlet-ui/src/option/props.ts | 4 + packages/varlet-ui/src/option/provide.ts | 5 +- .../__snapshots__/index.spec.js.snap | 16 +- packages/varlet-ui/src/radio/Radio.vue | 17 +- packages/varlet-ui/src/radio/radio.less | 21 -- packages/varlet-ui/src/select/Select.vue | 1 + packages/varlet-ui/src/select/docs/en-US.md | 1 + packages/varlet-ui/src/select/docs/zh-CN.md | 1 + .../src/select/useSelectController.ts | 18 +- packages/varlet-ui/src/utils/elements.ts | 19 +- packages/varlet-ui/tsconfig.json | 2 +- packages/varlet-ui/types/checkbox.d.ts | 2 +- packages/varlet-ui/types/checkboxGroup.d.ts | 4 +- packages/varlet-ui/types/menuSelect.d.ts | 10 +- packages/varlet-ui/types/radioGroup.d.ts | 4 +- packages/varlet-ui/types/select.d.ts | 2 +- 46 files changed, 1195 insertions(+), 175 deletions(-) create mode 100644 packages/varlet-ui/src/menu-select/MenuChildren.vue diff --git a/packages/varlet-ui/src/checkbox-group/CheckboxGroup.vue b/packages/varlet-ui/src/checkbox-group/CheckboxGroup.vue index 10deca0ed08..358403bb6b4 100644 --- a/packages/varlet-ui/src/checkbox-group/CheckboxGroup.vue +++ b/packages/varlet-ui/src/checkbox-group/CheckboxGroup.vue @@ -102,17 +102,11 @@ export default defineComponent({ checkboxes.forEach(({ sync }) => sync(props.modelValue)) } - function resetWithAnimation() { - checkboxes.forEach((checkbox) => checkbox.resetWithAnimation()) - } - // expose function checkAll() { const checkedValues: any[] = checkboxes.map(({ checkedValue }) => checkedValue.value) const changedModelValue: any[] = uniq(checkedValues) - resetWithAnimation() - call(props['onUpdate:modelValue'], changedModelValue) return changedModelValue @@ -125,8 +119,6 @@ export default defineComponent({ .map(({ checkedValue }) => checkedValue.value) const changedModelValue: any[] = uniq(checkedValues) - resetWithAnimation() - call(props['onUpdate:modelValue'], changedModelValue) return changedModelValue diff --git a/packages/varlet-ui/src/checkbox-group/__tests__/__snapshots__/index.spec.js.snap b/packages/varlet-ui/src/checkbox-group/__tests__/__snapshots__/index.spec.js.snap index b5f73cf4c4a..024178cb2d6 100644 --- a/packages/varlet-ui/src/checkbox-group/__tests__/__snapshots__/index.spec.js.snap +++ b/packages/varlet-ui/src/checkbox-group/__tests__/__snapshots__/index.spec.js.snap @@ -123,7 +123,7 @@ exports[`test checkbox group label is function 2`] = `
- +
@@ -172,7 +172,7 @@ exports[`test checkbox group label is function 3`] = `
- +
@@ -185,7 +185,7 @@ exports[`test checkbox group label is function 3`] = `
- +
@@ -221,7 +221,7 @@ exports[`test checkbox group label is function 4`] = `
- +
@@ -235,7 +235,7 @@ exports[`test checkbox group label is function 4`] = `
- +
2-false
@@ -441,7 +441,7 @@ exports[`test checkbox group validation 2`] = `
- +
@@ -567,7 +567,7 @@ exports[`test checkbox validation 2`] = ` "
- +
@@ -610,7 +610,7 @@ exports[`validation with zod > checkbox 2`] = ` "
- +
@@ -677,7 +677,7 @@ exports[`validation with zod > checkbox group 2`] = `
- +
diff --git a/packages/varlet-ui/src/checkbox-group/__tests__/index.spec.js b/packages/varlet-ui/src/checkbox-group/__tests__/index.spec.js index a8983a7e57f..9e124aadd7f 100644 --- a/packages/varlet-ui/src/checkbox-group/__tests__/index.spec.js +++ b/packages/varlet-ui/src/checkbox-group/__tests__/index.spec.js @@ -72,7 +72,7 @@ test('test checkbox onClick & onChange', async () => { await wrapper.find('.var-checkbox').trigger('click') expect(onClick).toHaveBeenCalledTimes(1) - expect(onChange).lastCalledWith(true) + expect(onChange).lastCalledWith(true, false) wrapper.unmount() }) @@ -146,16 +146,20 @@ test('test checkbox readonly', async () => { wrapper.unmount() }) -test('test checkbox indeterminate', () => { +test('test checkbox indeterminate', async () => { + const onChange = vi.fn() const wrapper = mount(VarCheckbox, { props: { modelValue: false, indeterminate: true, + onChange, }, }) expect(wrapper.html()).toMatchSnapshot() + await wrapper.find('.var-checkbox').trigger('click') + expect(onChange).lastCalledWith(false, false) wrapper.unmount() }) diff --git a/packages/varlet-ui/src/checkbox/Checkbox.vue b/packages/varlet-ui/src/checkbox/Checkbox.vue index eb5cde1ce57..c058c231de2 100644 --- a/packages/varlet-ui/src/checkbox/Checkbox.vue +++ b/packages/varlet-ui/src/checkbox/Checkbox.vue @@ -19,28 +19,13 @@ @blur="isFocusing = false" > - + - + - + value.value === props.checkedValue) const checkedValue = computed(() => props.checkedValue) - const withAnimation = ref(false) const { checkboxGroup, bindCheckboxGroup } = useCheckboxGroup() const { hovering, handleHovering } = useHoverOverlay() const { form, bindForm } = useForm() @@ -118,7 +102,6 @@ export default defineComponent({ validate, resetValidation, reset, - resetWithAnimation, } call(bindCheckboxGroup, checkboxProvider) @@ -138,9 +121,7 @@ export default defineComponent({ const { checkedValue, onChange } = props value.value = changedValue - isIndeterminate.value = false - - call(onChange, value.value) + call(onChange, value.value, isIndeterminate.value) validateWithTrigger('onChange') changedValue === checkedValue ? checkboxGroup?.onChecked(checkedValue) : checkboxGroup?.onUnchecked(checkedValue) } @@ -158,7 +139,13 @@ export default defineComponent({ return } - withAnimation.value = true + if (isIndeterminate.value === true) { + isIndeterminate.value = false + call(props.onChange, value.value, isIndeterminate.value) + validateWithTrigger('onChange') + return + } + const maximum = checkboxGroup ? checkboxGroup.checkedCount.value >= Number(checkboxGroup.max.value) : false if (!checked.value && maximum) { @@ -177,10 +164,6 @@ export default defineComponent({ value.value = values.includes(checkedValue) ? checkedValue : uncheckedValue } - function resetWithAnimation() { - withAnimation.value = false - } - // expose function reset() { value.value = props.uncheckedValue @@ -235,7 +218,6 @@ export default defineComponent({ action, isFocusing, isIndeterminate, - withAnimation, checked, errorMessage, checkboxGroupErrorMessage: checkboxGroup?.errorMessage, diff --git a/packages/varlet-ui/src/checkbox/checkbox.less b/packages/varlet-ui/src/checkbox/checkbox.less index acde2bd47b2..5c0e547bc96 100644 --- a/packages/varlet-ui/src/checkbox/checkbox.less +++ b/packages/varlet-ui/src/checkbox/checkbox.less @@ -8,23 +8,6 @@ --checkbox-icon-size: 24px; } -@keyframes var-vibrate-animation { - 0% { - opacity: 1; - transform: scale(1); - } - - 50% { - opacity: 0.8; - transform: scale(0.8); - } - - 100% { - opacity: 1; - transform: scale(1); - } -} - .var-checkbox { display: flex; align-items: center; @@ -58,10 +41,6 @@ color: var(--checkbox-text-color); } - &--with-animation { - animation: var-vibrate-animation 0.25s; - } - &--checked { color: var(--checkbox-checked-color); } diff --git a/packages/varlet-ui/src/checkbox/docs/en-US.md b/packages/varlet-ui/src/checkbox/docs/en-US.md index 650c7c961a1..8738f6a6d4b 100644 --- a/packages/varlet-ui/src/checkbox/docs/en-US.md +++ b/packages/varlet-ui/src/checkbox/docs/en-US.md @@ -21,7 +21,7 @@ | Event | Description | Arguments | | --- | --- | --- | | `click` | Triggered on Click | `e: Event` | -| `change` | Triggered on change | `value: any` | +| `change` | Triggered on change | `value: any, indeterminate: boolean` | ### Slots diff --git a/packages/varlet-ui/src/checkbox/docs/zh-CN.md b/packages/varlet-ui/src/checkbox/docs/zh-CN.md index d701e1bf0a2..327a9639754 100644 --- a/packages/varlet-ui/src/checkbox/docs/zh-CN.md +++ b/packages/varlet-ui/src/checkbox/docs/zh-CN.md @@ -21,7 +21,7 @@ | 事件名 | 说明 | 参数 | | --- | --- | --- | | `click` | 点击时触发 | `e: Event` | -| `change` | 状态变更时触发 | `value: any` | +| `change` | 状态变更时触发 | `value: any, indeterminate: boolean` | ### 插槽 diff --git a/packages/varlet-ui/src/checkbox/props.ts b/packages/varlet-ui/src/checkbox/props.ts index ec67612e018..a439ce95323 100644 --- a/packages/varlet-ui/src/checkbox/props.ts +++ b/packages/varlet-ui/src/checkbox/props.ts @@ -32,7 +32,7 @@ export const props = { }, rules: [Array, Function, Object] as PropType, onClick: defineListenerProp<(e: Event) => void>(), - onChange: defineListenerProp<(value: any) => void>(), + onChange: defineListenerProp<(value: any, indeterminate: boolean) => void>(), 'onUpdate:modelValue': defineListenerProp<(value: any) => void>(), 'onUpdate:indeterminate': defineListenerProp<(value: boolean) => void>(), } diff --git a/packages/varlet-ui/src/checkbox/provide.ts b/packages/varlet-ui/src/checkbox/provide.ts index 978e8c49e28..e9aed0a8c18 100644 --- a/packages/varlet-ui/src/checkbox/provide.ts +++ b/packages/varlet-ui/src/checkbox/provide.ts @@ -7,7 +7,6 @@ export interface CheckboxProvider extends Validation { checkedValue: ComputedRef checked: ComputedRef sync(values: Array): void - resetWithAnimation(): void } export function useCheckboxGroup() { diff --git a/packages/varlet-ui/src/menu-option/MenuOption.vue b/packages/varlet-ui/src/menu-option/MenuOption.vue index 1eadfa6559e..a02e6eac37a 100644 --- a/packages/varlet-ui/src/menu-option/MenuOption.vue +++ b/packages/varlet-ui/src/menu-option/MenuOption.vue @@ -2,7 +2,14 @@
- +
+ +
+ +
diff --git a/packages/varlet-ui/src/menu-select/MenuSelect.vue b/packages/varlet-ui/src/menu-select/MenuSelect.vue index 76ca18af85c..862203119d0 100644 --- a/packages/varlet-ui/src/menu-select/MenuSelect.vue +++ b/packages/varlet-ui/src/menu-select/MenuSelect.vue @@ -31,17 +31,29 @@ ref="menuOptionsRef" :class="classes(n('menu'), formatElevation(elevation, 3), [scrollable, n('--scrollable')])" > - @@ -51,8 +63,9 @@ + + +``` + ### Size ```html @@ -246,6 +270,110 @@ const options = ref([ ``` +### Cascade + +An array of options may be passed to the `children` attribute of options to achieve a cascading effect. + +```html + + + +``` + +### Multiple Cascade + +Cascading multiple selections can be achieved by setting the `multiple` attribute on the basis of cascading single selections. + +```html + + + +``` + ### Options API With Customized Key You can pass the options as an array of objects to the `options` property. Use the `label-key` and `value-key` properties to specify the fields for the label and value within the options array. @@ -308,6 +436,7 @@ const options = ref([ | `options` ***3.3.7*** | Specifies options | _MenuSelectOption[]_ | `[]` | | `label-key` ***3.3.7*** | As the key that uniquely identifies label | _string_ | `label` | | `value-key` ***3.3.7*** | As the key that uniquely identifies value | _string_ | `value` | +| `children-key` ***3.8.0*** | As the key that uniquely identifies children | _string_ | `children` | #### MenuSelectOption @@ -315,6 +444,7 @@ const options = ref([ | ------- | --- |----------------|-----------| | `label` | The text of option | _string \| VNode \| (option: MenuSelectOption, selected: boolean) => VNodeChild_ | `-` | | `value` | The value of option | _any_ | `-` | +| `children` ***3.8.0*** | The children options of option | _MenuSelectOption[]_ | `-` | | `disabled` | Whether to disable option | _boolean_ | `-` | | `ripple` | Whether to enable ripple | _boolean_ | `true` | @@ -374,6 +504,7 @@ const options = ref([ | `close` | Triggered when the menu is closed | `-` | | `closed` | Triggered when the closing menu animation ends | `-` | | `click-outside` | Triggered when clicking outside the menu | `event: Event` | +| `select` ***3.8.0*** | Triggered when selecting a option | `value: any, option: MenuSelectOption` | ### Slots diff --git a/packages/varlet-ui/src/menu-select/docs/zh-CN.md b/packages/varlet-ui/src/menu-select/docs/zh-CN.md index d1530b51381..fdf72abbb50 100644 --- a/packages/varlet-ui/src/menu-select/docs/zh-CN.md +++ b/packages/varlet-ui/src/menu-select/docs/zh-CN.md @@ -26,6 +26,30 @@ const value = ref() ``` +### 选中事件 + +```html + + + +``` + ### 尺寸 ```html @@ -246,6 +270,110 @@ const options = ref([ ``` +### 级联单选 + +可以将选项数组传递给选项的 `children` 属性以实现级联效果。 + +```html + + + +``` + +### 级联多选 + +在级联单选的基础上设置 `multiple` 属性即可实现级联多选。 + +```html + + + +``` + ### 选项式 API(自定义字段) 可以将选项以数组形式传给 `options` 属性,同时通过 `label-key` 和 `value-key` 属性指定选项数组内文本和值的字段。 @@ -308,6 +436,7 @@ const options = ref([ | `options` ***3.3.7*** | 指定可选项 | _MenuSelectOption[]_ | `[]` | | `label-key` ***3.3.7*** | 作为 label 唯一标识的键名 | _string_ | `label` | | `value-key` ***3.3.7*** | 作为 value 唯一标识的键名 | _string_ | `value` | +| `children-key` ***3.8.0*** | 作为 children 唯一标识的键名 | _string_ | `children` | #### MenuSelectOption @@ -315,6 +444,7 @@ const options = ref([ | ------- | --- |----------------|-----------| | `label` | 选项的标签 | _string \| VNode \| (option: MenuSelectOption, selected: boolean) => VNodeChild_ | `-` | | `value` | 选项的值 | _any_ | `-` | +| `children` ***3.8.0*** | 选项的子选项 | _MenuSelectOption[]_ | `-` | | `disabled` | 是否禁用 | _boolean_ | `-` | | `ripple` | 是否启用水波效果 | _boolean_ | `true` | @@ -374,6 +504,7 @@ const options = ref([ | `close` | 关闭菜单时触发 | `-` | | `closed` | 关闭菜单动画结束时触发 | `-` | | `click-outside` | 点击菜单外部时触发 | `event: Event` | +| `select` ***3.8.0*** | 选择某个选项时触发 | `value: any, option: MenuSelectOption` | ### 插槽 diff --git a/packages/varlet-ui/src/menu-select/example/index.vue b/packages/varlet-ui/src/menu-select/example/index.vue index bc6f34f4cfc..73464071f11 100644 --- a/packages/varlet-ui/src/menu-select/example/index.vue +++ b/packages/varlet-ui/src/menu-select/example/index.vue @@ -2,6 +2,7 @@ import { computed, ref } from 'vue' import { watchLang, onThemeChange, AppType } from '@varlet/cli/client' import { use, t } from './locale' +import { Snackbar } from '@varlet/ui' const value = ref() const valueNormal = ref() @@ -47,6 +48,43 @@ const keyedSelectOptions = computed(() => [ }, ]) +const cascadeValue = ref() +const cascadeMultipleValue = ref([]) +const cascadeOptions = ref([ + { + label: '1', + value: 1, + }, + { + label: '2', + value: 2, + children: [ + { + label: '2-1', + value: 21, + children: [ + { + label: '2-1-1', + value: 211, + }, + { + label: '2-1-2', + value: 212, + }, + ], + }, + { + label: '2-2', + value: 22, + }, + ], + }, + { + label: '3', + value: 3, + }, +]) + watchLang((lang) => { use(lang) value.value = undefined @@ -59,8 +97,15 @@ watchLang((lang) => { valueScrollable.value = undefined valueCloseOnSelect.value = undefined valueMultiple.value = [] + cascadeValue.value = undefined + cascadeMultipleValue.value = [] }) + onThemeChange() + +function handleSelect(value) { + Snackbar(`Select: ${value}`) +} + {{ t('onSelect') }} + + {{ t('please') }} + + + + {{ t('size') }} @@ -178,6 +234,16 @@ onThemeChange() {{ valueSelectOptions ? valueSelectOptions : t('please') }} + {{ t('cascade') }} + + {{ cascadeValue ? cascadeValue : t('please') }} + + + {{ t('multipleCascade') }} + + {{ cascadeMultipleValue.length ? cascadeMultipleValue : t('please') }} + + {{ t('selectOptionsWithCustomizedKey') }} {{ valueKeyedSelectOptions ? valueKeyedSelectOptions : t('please') }} diff --git a/packages/varlet-ui/src/menu-select/example/locale/en-US.ts b/packages/varlet-ui/src/menu-select/example/locale/en-US.ts index 4a6dd0d51df..94a79a9eb52 100644 --- a/packages/varlet-ui/src/menu-select/example/locale/en-US.ts +++ b/packages/varlet-ui/src/menu-select/example/locale/en-US.ts @@ -18,4 +18,7 @@ export default { closeOnSelect: 'Disable Close On Select', selectOptions: 'Options API', selectOptionsWithCustomizedKey: 'Options API (With Customized Key)', + onSelect: 'Selected Event', + cascade: 'Cascade', + multipleCascade: 'Multiple Cascade', } diff --git a/packages/varlet-ui/src/menu-select/example/locale/zh-CN.ts b/packages/varlet-ui/src/menu-select/example/locale/zh-CN.ts index 42baecff0e2..b9d8ed53a8f 100644 --- a/packages/varlet-ui/src/menu-select/example/locale/zh-CN.ts +++ b/packages/varlet-ui/src/menu-select/example/locale/zh-CN.ts @@ -18,4 +18,7 @@ export default { closeOnSelect: '选择选项时禁止关闭菜单', selectOptions: '选项式 API', selectOptionsWithCustomizedKey: '选项式 API(自定义字段)', + onSelect: '选中事件', + cascade: '级联单选', + multipleCascade: '级联多选', } diff --git a/packages/varlet-ui/src/menu-select/menuSelect.less b/packages/varlet-ui/src/menu-select/menuSelect.less index 8cc5fa557e3..d5fc065c22e 100644 --- a/packages/varlet-ui/src/menu-select/menuSelect.less +++ b/packages/varlet-ui/src/menu-select/menuSelect.less @@ -17,3 +17,8 @@ max-height: var(--menu-select-menu-max-height); } } + +.var-menu-children[var-menu-children-cover] { + width: 100%; + display: block; +} diff --git a/packages/varlet-ui/src/menu-select/props.ts b/packages/varlet-ui/src/menu-select/props.ts index a27aef163bc..a820decde26 100644 --- a/packages/varlet-ui/src/menu-select/props.ts +++ b/packages/varlet-ui/src/menu-select/props.ts @@ -11,9 +11,14 @@ export type MenuSelectOptionLabel = string | VNode | MenuSelectOptionLabelRender export interface MenuSelectOption { label?: MenuSelectOptionLabel value?: any + children?: MenuSelectOption[] disabled?: boolean ripple?: boolean [key: PropertyKey]: any + + _parent?: MenuSelectOption + _children?: MenuSelectOption[] + _rawOption?: MenuSelectOption } export const props = { @@ -33,6 +38,10 @@ export const props = { type: String, default: 'value', }, + childrenKey: { + type: String, + default: 'children', + }, size: { type: String as PropType, default: 'normal', @@ -44,6 +53,7 @@ export const props = { default: true, }, 'onUpdate:modelValue': defineListenerProp<(value: any) => void>(), + onSelect: defineListenerProp<(value: any, option: MenuSelectOption) => void>(), ...pickProps(menuProps, [ 'show', 'disabled', diff --git a/packages/varlet-ui/src/menu/Menu.vue b/packages/varlet-ui/src/menu/Menu.vue index 5bbff9daf04..675535e0d73 100644 --- a/packages/varlet-ui/src/menu/Menu.vue +++ b/packages/varlet-ui/src/menu/Menu.vue @@ -55,6 +55,7 @@ export default defineComponent({ handlePopoverMouseleave, handlePopoverClose, handleClosed, + setAllowClose, // expose open, // expose @@ -65,6 +66,10 @@ export default defineComponent({ setReference, } = usePopover(props) + function allowClose() { + setAllowClose(true) + } + return { popover, host, @@ -72,6 +77,7 @@ export default defineComponent({ show, zIndex, teleportDisabled, + allowClose, formatElevation, toSizeUnit, n, diff --git a/packages/varlet-ui/src/menu/menu.less b/packages/varlet-ui/src/menu/menu.less index bd91df39bff..459965ece1f 100644 --- a/packages/varlet-ui/src/menu/menu.less +++ b/packages/varlet-ui/src/menu/menu.less @@ -4,7 +4,7 @@ } .var-menu { - display: inline-block; + display: inline-flex; outline: none; &__menu { diff --git a/packages/varlet-ui/src/menu/props.ts b/packages/varlet-ui/src/menu/props.ts index 507f3272911..bc6ce0c9c0c 100644 --- a/packages/varlet-ui/src/menu/props.ts +++ b/packages/varlet-ui/src/menu/props.ts @@ -52,4 +52,8 @@ export const props = { onClosed: defineListenerProp<() => void>(), onClickOutside: defineListenerProp<(event: Event) => void>(), 'onUpdate:show': defineListenerProp<(show: boolean) => void>(), + + // internal start + cascadeOptimization: Boolean, + // internal end } diff --git a/packages/varlet-ui/src/menu/usePopover.ts b/packages/varlet-ui/src/menu/usePopover.ts index f13830c60f9..faf6d000fb6 100644 --- a/packages/varlet-ui/src/menu/usePopover.ts +++ b/packages/varlet-ui/src/menu/usePopover.ts @@ -55,6 +55,7 @@ export interface UsePopoverOptions { onClose?: ListenerProp<() => void> onClosed?: ListenerProp<() => void> onClickOutside?: ListenerProp<(event: Event) => void> + cascadeOptimization?: boolean 'onUpdate:show'?: ListenerProp<(show: boolean) => void> } @@ -81,6 +82,7 @@ export function usePopover(options: UsePopoverOptions) { let reference: Reference | undefined = undefined let enterPopover = false let enterReference = false + let allowClose = true useEventListener(() => window, 'keydown', handleKeydown) watch(() => [options.offsetX, options.offsetY, options.placement, options.strategy], resize) @@ -207,6 +209,10 @@ export function usePopover(options: UsePopoverOptions) { } enterPopover = true + + if (options.cascadeOptimization) { + allowClose = false + } } async function handlePopoverMouseleave() { @@ -413,6 +419,10 @@ export function usePopover(options: UsePopoverOptions) { return targetReference } + function setAllowClose(value: boolean) { + allowClose = value + } + function setReference(newReference: Reference) { destroyPopperInstance() reference = newReference @@ -444,6 +454,10 @@ export function usePopover(options: UsePopoverOptions) { // expose function close() { + if (!allowClose) { + return + } + show.value = false call(options['onUpdate:show'], false) } @@ -459,6 +473,7 @@ export function usePopover(options: UsePopoverOptions) { handlePopoverMouseleave, handleClosed, setReference, + setAllowClose, resize, open, close, diff --git a/packages/varlet-ui/src/option/Option.vue b/packages/varlet-ui/src/option/Option.vue index 3aa861736eb..ac773feee01 100644 --- a/packages/varlet-ui/src/option/Option.vue +++ b/packages/varlet-ui/src/option/Option.vue @@ -6,7 +6,7 @@ color: optionSelected ? focusColor : undefined, }" :tabindex="disabled ? undefined : '-1'" - v-ripple="{ disabled }" + v-ripple="{ disabled: disabled || !ripple }" v-hover:desktop="handleHovering" @focus="isFocusing = true" @blur="isFocusing = false" @@ -67,6 +67,13 @@ export default defineComponent({ const isFocusing = ref(false) const optionSelected = ref(false) const selected = computed(() => optionSelected.value) + const value = computed(() => props.value) + const disabled = computed(() => props.disabled) + const ripple = computed(() => props.ripple) + const { select, bindSelect } = useSelect() + const { multiple, focusColor, onSelect, computeLabel } = select + const { hovering, handleHovering } = useHoverOverlay() + const labelVNode = computed(() => isFunction(props.label) ? props.label( @@ -79,14 +86,12 @@ export default defineComponent({ ) : props.label ) - const value = computed(() => props.value) - const { select, bindSelect } = useSelect() - const { multiple, focusColor, onSelect, computeLabel } = select - const { hovering, handleHovering } = useHoverOverlay() const optionProvider: OptionProvider = { label: labelVNode, value, + disabled, + ripple, selected, sync, } diff --git a/packages/varlet-ui/src/option/docs/en-US.md b/packages/varlet-ui/src/option/docs/en-US.md index e71d74c5879..4ea433874cf 100644 --- a/packages/varlet-ui/src/option/docs/en-US.md +++ b/packages/varlet-ui/src/option/docs/en-US.md @@ -7,6 +7,7 @@ | `label` | The text that the option displays | _any_ | `-` | | `value` | The value of the option binding | _any_ | `-` | | `disabled` | Whether to disable | _boolean_ | `false` | +| `ripple` ***3.8.0*** | Whether to enable ripple | _boolean_ | `true` | ### Slots diff --git a/packages/varlet-ui/src/option/docs/zh-CN.md b/packages/varlet-ui/src/option/docs/zh-CN.md index 94db5c269b7..95b5a02933e 100644 --- a/packages/varlet-ui/src/option/docs/zh-CN.md +++ b/packages/varlet-ui/src/option/docs/zh-CN.md @@ -7,6 +7,7 @@ | `label` | 选项显示的文本 | _any_ | `-` | | `value` | 选项绑定的值 | _any_ | `-` | | `disabled` | 是否禁用 | _boolean_ | `false` | +| `ripple` ***3.8.0*** | 是否启用水波效果 | _boolean_ | `true` | ### 插槽 diff --git a/packages/varlet-ui/src/option/props.ts b/packages/varlet-ui/src/option/props.ts index ba1d366ae5a..cf117fe3242 100644 --- a/packages/varlet-ui/src/option/props.ts +++ b/packages/varlet-ui/src/option/props.ts @@ -5,6 +5,10 @@ export const props = { label: {}, value: {}, disabled: Boolean, + ripple: { + type: Boolean, + default: true, + }, // internal option: Object as PropType, } diff --git a/packages/varlet-ui/src/option/provide.ts b/packages/varlet-ui/src/option/provide.ts index 3ff9b2dcb41..547294a37dd 100644 --- a/packages/varlet-ui/src/option/provide.ts +++ b/packages/varlet-ui/src/option/provide.ts @@ -6,8 +6,11 @@ import { SELECT_BIND_OPTION_KEY, type SelectProvider } from '../select/provide' export interface OptionProvider { label: ComputedRef value: ComputedRef + disabled: ComputedRef + ripple: ComputedRef selected: ComputedRef - sync(checked: boolean): void + indeterminate?: ComputedRef + sync(checked: boolean, indeterminate?: boolean): void } export function useSelect() { diff --git a/packages/varlet-ui/src/radio-group/__tests__/__snapshots__/index.spec.js.snap b/packages/varlet-ui/src/radio-group/__tests__/__snapshots__/index.spec.js.snap index e093a319f79..500f311ef93 100644 --- a/packages/varlet-ui/src/radio-group/__tests__/__snapshots__/index.spec.js.snap +++ b/packages/varlet-ui/src/radio-group/__tests__/__snapshots__/index.spec.js.snap @@ -97,7 +97,7 @@ exports[`test radio group label is function 2`] = `
-
+
0-false
@@ -108,7 +108,7 @@ exports[`test radio group label is function 2`] = `
-
+
1-true
@@ -140,7 +140,7 @@ exports[`test radio group label is function 3`] = `
-
+
0-false
@@ -151,7 +151,7 @@ exports[`test radio group label is function 3`] = `
-
+
1-true
@@ -347,7 +347,7 @@ exports[`test radio group validation 2`] = `
-
+
@@ -445,7 +445,7 @@ exports[`test radio validation 1`] = ` exports[`test radio validation 2`] = ` "
-
+
@@ -484,7 +484,7 @@ exports[`test validation with zod > radio 1`] = ` exports[`test validation with zod > radio 2`] = ` "
-
+
@@ -545,7 +545,7 @@ exports[`test validation with zod > radio group 2`] = `
-
+
diff --git a/packages/varlet-ui/src/radio/Radio.vue b/packages/varlet-ui/src/radio/Radio.vue index 4b9b5821c64..921c29dca52 100644 --- a/packages/varlet-ui/src/radio/Radio.vue +++ b/packages/varlet-ui/src/radio/Radio.vue @@ -19,20 +19,10 @@ @blur="isFocusing = false" > - + - + value.value === props.checkedValue) - const withAnimation = ref(false) const { radioGroup, bindRadioGroup } = useRadioGroup() const { hovering, handleHovering } = useHoverOverlay() const { form, bindForm } = useForm() @@ -183,7 +172,6 @@ export default defineComponent({ return } - withAnimation.value = true change(checked.value ? uncheckedValue : checkedValue) } @@ -222,7 +210,6 @@ export default defineComponent({ return { action, isFocusing, - withAnimation, checked, errorMessage, radioGroupErrorMessage: radioGroup?.errorMessage, diff --git a/packages/varlet-ui/src/radio/radio.less b/packages/varlet-ui/src/radio/radio.less index 28c57dc6c31..08f39e1b76c 100644 --- a/packages/varlet-ui/src/radio/radio.less +++ b/packages/varlet-ui/src/radio/radio.less @@ -8,23 +8,6 @@ --radio-text-color: #555; } -@keyframes var-vibrate-animation { - 0% { - opacity: 1; - transform: scale(1); - } - - 50% { - opacity: 0.8; - transform: scale(0.8); - } - - 100% { - opacity: 1; - transform: scale(1); - } -} - .var-radio { display: flex; align-items: center; @@ -58,10 +41,6 @@ color: var(--radio-text-color); } - &--with-animation[var-radio-cover] { - animation: var-vibrate-animation 0.25s; - } - &--checked { color: var(--radio-checked-color); } diff --git a/packages/varlet-ui/src/select/Select.vue b/packages/varlet-ui/src/select/Select.vue index a92c3423081..8d0ffc2c128 100644 --- a/packages/varlet-ui/src/select/Select.vue +++ b/packages/varlet-ui/src/select/Select.vue @@ -120,6 +120,7 @@ :value="option[valueKey]" :option="option" :disabled="option.disabled" + :ripple="option.ripple" /> diff --git a/packages/varlet-ui/src/select/docs/en-US.md b/packages/varlet-ui/src/select/docs/en-US.md index 956752d954d..92d9feb1abd 100644 --- a/packages/varlet-ui/src/select/docs/en-US.md +++ b/packages/varlet-ui/src/select/docs/en-US.md @@ -488,6 +488,7 @@ const keyOptions = ref([ | `label` | The text of option | _string \| VNode \| (option: SelectOption, selected: boolean) => VNodeChild_ | `-` | | `value` | The value of option | _any_ | `-` | | `disabled` | Whether to disable option | _boolean_ | `-` | +| `ripple` ***3.3.0*** | Whether to enable ripple | _boolean_ | `true` | #### Option Props diff --git a/packages/varlet-ui/src/select/docs/zh-CN.md b/packages/varlet-ui/src/select/docs/zh-CN.md index 990d96386f1..bc38009d474 100644 --- a/packages/varlet-ui/src/select/docs/zh-CN.md +++ b/packages/varlet-ui/src/select/docs/zh-CN.md @@ -497,6 +497,7 @@ const keyOptions = ref([ | `label` | 选项显示的文本 | _any_ | `-` | | `value` | 选项绑定的值 | _any_ | `-` | | `disabled` | 是否禁用 | _boolean_ | `false` | +| `ripple` ***3.8.0*** | 是否启用水波效果 | _boolean_ | `true` | ### 方法 diff --git a/packages/varlet-ui/src/select/useSelectController.ts b/packages/varlet-ui/src/select/useSelectController.ts index efc966d0cb7..00a16cff361 100644 --- a/packages/varlet-ui/src/select/useSelectController.ts +++ b/packages/varlet-ui/src/select/useSelectController.ts @@ -7,6 +7,7 @@ export interface UseSelectControllerOptions { multiple: () => boolean optionProviders: () => OptionProvider[] optionProvidersLength: () => number + optionIsIndeterminate?: (option: OptionProvider) => boolean } export function useSelectController(options: UseSelectControllerOptions) { @@ -15,6 +16,7 @@ export function useSelectController(options: UseSelectControllerOptions) { modelValue: modelValueGetter, optionProviders: optionProvidersGetter, optionProvidersLength: optionProvidersLengthGetter, + optionIsIndeterminate, } = options const label = ref('') const labels = ref([]) @@ -51,7 +53,7 @@ export function useSelectController(options: UseSelectControllerOptions) { return option?.label.value ?? '' } - function findValueOrLabel({ value, label }: OptionProvider) { + function getOptionProviderKey({ value, label }: OptionProvider) { return value.value ?? label.value } @@ -59,7 +61,9 @@ export function useSelectController(options: UseSelectControllerOptions) { const multiple = multipleGetter() const options = optionProvidersGetter() - return multiple ? options.filter(({ selected }) => selected.value).map(findValueOrLabel) : findValueOrLabel(option) + return multiple + ? options.filter(({ selected }) => selected.value).map(getOptionProviderKey) + : getOptionProviderKey(option) } function syncOptions() { @@ -68,9 +72,14 @@ export function useSelectController(options: UseSelectControllerOptions) { const options = optionProvidersGetter() if (multiple) { - options.forEach((option) => option.sync(modelValue.includes(findValueOrLabel(option)))) + options.forEach((option) => + option.sync( + modelValue.includes(getOptionProviderKey(option)), + optionIsIndeterminate ? optionIsIndeterminate(option) : undefined + ) + ) } else { - options.forEach((option) => option.sync(modelValue === findValueOrLabel(option))) + options.forEach((option) => option.sync(modelValue === getOptionProviderKey(option))) } computeLabel() @@ -79,6 +88,7 @@ export function useSelectController(options: UseSelectControllerOptions) { return { label, labels, + getOptionProviderKey, computeLabel, getSelectedValue, } diff --git a/packages/varlet-ui/src/utils/elements.ts b/packages/varlet-ui/src/utils/elements.ts index 671b6f5aa82..3e0edbf130f 100644 --- a/packages/varlet-ui/src/utils/elements.ts +++ b/packages/varlet-ui/src/utils/elements.ts @@ -247,6 +247,20 @@ export function padStartFlex(style: string | undefined) { return style === 'start' || style === 'end' ? `flex-${style}` : style } +export function isDisplayNoneElement(element: HTMLElement) { + let parent: HTMLElement | null = element + + while (parent && parent !== document.documentElement) { + if (getStyle(parent).display === 'none') { + return true + } + + parent = parent.parentNode as HTMLElement | null + } + + return false +} + const focusableSelector = ['button', 'input', 'select', 'textarea', '[tabindex]', '[href]'] .map((s) => `${s}:not([disabled])`) .join(', ') @@ -256,7 +270,10 @@ export function focusChildElementByKey( parentElement: HTMLElement, key: 'ArrowDown' | 'ArrowUp' ) { - const focusableElements = parentElement.querySelectorAll(focusableSelector) + const focusableElements = Array.from(parentElement.querySelectorAll(focusableSelector)).filter( + (element) => !isDisplayNoneElement(element) + ) + if (!focusableElements.length) { return } diff --git a/packages/varlet-ui/tsconfig.json b/packages/varlet-ui/tsconfig.json index f90fbd3fdc7..d7912ac9f36 100644 --- a/packages/varlet-ui/tsconfig.json +++ b/packages/varlet-ui/tsconfig.json @@ -8,5 +8,5 @@ "allowJs": true, "types": ["vitest/globals"] }, - "include": ["src/**/*.{ts,tsx,vue}"] + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], } diff --git a/packages/varlet-ui/types/checkbox.d.ts b/packages/varlet-ui/types/checkbox.d.ts index dc87dfb7ba6..3842ee6be2a 100644 --- a/packages/varlet-ui/types/checkbox.d.ts +++ b/packages/varlet-ui/types/checkbox.d.ts @@ -19,7 +19,7 @@ export interface CheckboxProps extends BasicAttributes { validateTrigger?: Array rules?: CheckboxRules onClick?: ListenerProp<(e: Event) => void> - onChange?: ListenerProp<(value: any) => void> + onChange?: ListenerProp<(value: any, indeterminate: boolean) => void> 'onUpdate:modelValue'?: ListenerProp<(value: any) => void> 'onUpdate:indeterminate'?: ListenerProp<(value: boolean) => void> } diff --git a/packages/varlet-ui/types/checkboxGroup.d.ts b/packages/varlet-ui/types/checkboxGroup.d.ts index 684e091c811..d70bf260596 100644 --- a/packages/varlet-ui/types/checkboxGroup.d.ts +++ b/packages/varlet-ui/types/checkboxGroup.d.ts @@ -29,9 +29,9 @@ export interface CheckboxGroupProps extends BasicAttributes { max?: string | number labelKey?: string valueKey?: string - options?: Array + options?: CheckboxGroupOption[] direction?: CheckboxGroupDirection - validateTrigger?: Array + validateTrigger?: CheckboxGroupValidateTrigger[] rules?: CheckboxGroupRules onChange?: ListenerProp<(value: Array) => void> 'onUpdate:modelValue'?: ListenerProp<(value: Array) => void> diff --git a/packages/varlet-ui/types/menuSelect.d.ts b/packages/varlet-ui/types/menuSelect.d.ts index f756d57f767..7908e7ec5c6 100644 --- a/packages/varlet-ui/types/menuSelect.d.ts +++ b/packages/varlet-ui/types/menuSelect.d.ts @@ -30,13 +30,17 @@ export type MenuSelectOptionLabelRender = (option: MenuSelectOption, checked: bo export interface MenuSelectOption { label?: string | VNode | MenuSelectOptionLabelRender - disabled?: boolean value?: any + disabled?: boolean ripple?: boolean + children?: MenuSelectOption[] + + [key: PropertyKey]: any } export interface MenuSelectProps extends BasicAttributes { modelValue?: any + options?: MenuSelectOption[] size?: MenuSelectSize multiple?: boolean scrollable?: boolean @@ -47,6 +51,9 @@ export interface MenuSelectProps extends BasicAttributes { reference?: MenuSelectReference placement?: MenuSelectPlacement strategy?: MenuSelectStrategy + labelKey?: string + valueKey?: string + childrenKey?: string offsetX?: string | number offsetY?: string | number teleport?: TeleportProps['to'] | false @@ -58,6 +65,7 @@ export interface MenuSelectProps extends BasicAttributes { onOpened?: ListenerProp<() => void> onClose?: ListenerProp<() => void> onClosed?: ListenerProp<() => void> + onSelect?: ListenerProp<(value: any) => void> 'onUpdate:modelValue'?: ListenerProp<(value: any) => void> 'onUpdate:show'?: ListenerProp<(show: boolean) => void> } diff --git a/packages/varlet-ui/types/radioGroup.d.ts b/packages/varlet-ui/types/radioGroup.d.ts index 8fc5b381f19..f3ab544c358 100644 --- a/packages/varlet-ui/types/radioGroup.d.ts +++ b/packages/varlet-ui/types/radioGroup.d.ts @@ -27,10 +27,10 @@ export { RadioGroupDirection } export interface RadioGroupProps extends BasicAttributes { modelValue?: any direction?: RadioGroupDirection - options?: Array + options?: RadioGroupOption[] labelKey?: string valueKey?: string - validateTrigger?: Array + validateTrigger?: RadioGroupValidateTrigger[] rules?: RadioGroupRules onChange?: ListenerProp<(value: any) => void> 'onUpdate:modelValue'?: ListenerProp<(value: any) => void> diff --git a/packages/varlet-ui/types/select.d.ts b/packages/varlet-ui/types/select.d.ts index 7863b536237..ae9e3dc0632 100644 --- a/packages/varlet-ui/types/select.d.ts +++ b/packages/varlet-ui/types/select.d.ts @@ -30,7 +30,7 @@ export interface SelectOption { export interface SelectProps extends BasicAttributes { modelValue?: any - options?: Array + options?: SelectOption[] labelKey?: string valueKey?: string variant?: SelectVariant