Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/.vitepress/plugins/badge/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type BadgeValue = BadgeType | string
*/
export const BADGE_TEXT_MAP: Record<BadgeType, string> = {
new: '新增',
deprecated: '已废弃',
deprecated: '弃用',
beta: 'Beta',
alpha: 'Alpha',
}
Expand Down
15 changes: 9 additions & 6 deletions docs/.vitepress/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,13 @@ div:has(h1[id='bubble-气泡组件']) {
}

.version-badge--deprecated {
background-color: #fef0f0;
color: #f56c6c;
border: 1px solid #fde2e2;
padding: 1px 6px;
background-color: var(--vp-c-bg-soft);
color: var(--vp-c-text-3);
border: 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
}

.version-badge--beta {
Expand All @@ -353,9 +357,8 @@ div:has(h1[id='bubble-气泡组件']) {
}

.dark .version-badge--deprecated {
background-color: rgba(245, 108, 108, 0.15);
color: #f89898;
border-color: rgba(245, 108, 108, 0.25);
background-color: rgba(255, 255, 255, 0.08);
color: var(--vp-c-text-2);
}

.dark .version-badge--beta {
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/themeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const sharedSidebarItems = [
text: '组件',
base: '/components/',
items: [
{ text: 'Container 容器', link: 'container' },
{ text: 'Layout 布局', link: 'layout' },
{ text: 'Bubble 气泡', link: 'bubble' },
{ text: 'Sender 消息输入框', link: 'sender' },
Expand All @@ -30,6 +29,7 @@ const sharedSidebarItems = [
{ text: 'Attachments 附件卡片', link: 'attachments' },
{ text: 'McpServerPicker 插件选择器', link: 'mcp-server-picker' },
{ text: 'Theme 主题', link: 'theme' },
{ text: 'Container 容器', link: 'container' },
],
},
{
Expand Down
4 changes: 3 additions & 1 deletion docs/demos/history/basic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
:data="data"
:selected="selected"
:show-rename-controls="isTouchDevice"
rename-control-on-click-outside="cancel"
:rename-control-on-click-outside="isTouchDevice ? 'cancel' : 'confirm'"
@item-click="(item) => (selected = item.id)"
@item-title-change="(newTitle, item) => (item.title = newTitle)"
@item-action="(item) => console.log(item)"
Expand All @@ -13,6 +13,8 @@
<tr-history
:data="groups"
:selected="selected2"
:show-rename-controls="isTouchDevice"
:rename-control-on-click-outside="isTouchDevice ? 'cancel' : 'confirm'"
@item-click="(item) => (selected2 = item.id)"
@item-title-change="(newTitle, item) => (item.title = newTitle)"
@item-action="(item) => console.log(item)"
Expand Down
6 changes: 5 additions & 1 deletion docs/demos/history/custom-menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
:data="data"
:selected="selected"
:menu-items="menuItems"
:show-rename-controls="isTouchDevice"
:rename-control-on-click-outside="isTouchDevice ? 'cancel' : 'confirm'"
@item-click="(item) => (selected = item.id)"
@item-title-change="(newTitle, item) => (item.title = newTitle)"
@item-action="(action, item) => handleAction(action, item)"
/>
</template>

<script setup lang="ts">
import { TrHistory } from '@opentiny/tiny-robot'
import { TrHistory, useTouchDevice } from '@opentiny/tiny-robot'
import { IconEditPen, IconDelete, IconCopy } from '@opentiny/tiny-robot-svgs'
import { reactive, ref } from 'vue'

const { isTouchDevice } = useTouchDevice()

const data = reactive([
{ title: '如何训练一只聪明的小狗', id: '1' },
{ title: 'How to make a perfect soufflé', id: '2' },
Expand Down
2 changes: 1 addition & 1 deletion docs/demos/history/icon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
:data="data"
:selected="selected"
:show-rename-controls="isTouchDevice"
rename-control-on-click-outside="cancel"
:rename-control-on-click-outside="isTouchDevice ? 'cancel' : 'confirm'"
@item-click="(item) => (selected = item.id)"
@item-title-change="(newTitle, item) => (item.title = newTitle)"
@item-action="(item) => console.log(item)"
Expand Down
2 changes: 1 addition & 1 deletion docs/demos/history/slot-item-prefix.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
:data="data"
:selected="selected"
:show-rename-controls="isTouchDevice"
rename-control-on-click-outside="cancel"
:rename-control-on-click-outside="isTouchDevice ? 'cancel' : 'confirm'"
@item-click="(item) => (selected = item.id)"
@item-title-change="(newTitle, item) => (item.title = newTitle)"
@item-action="(item) => console.log(item)"
Expand Down
2 changes: 1 addition & 1 deletion docs/demos/history/slot-item-title.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
:data="data"
:selected="selected"
:show-rename-controls="isTouchDevice"
rename-control-on-click-outside="cancel"
:rename-control-on-click-outside="isTouchDevice ? 'cancel' : 'confirm'"
@item-click="(item) => (selected = item.id)"
@item-title-change="(newTitle, item) => (item.title = newTitle)"
@item-action="(item) => console.log(item)"
Expand Down
4 changes: 4 additions & 0 deletions docs/src/components/container.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
---
outline: [1, 3]
badge: deprecated
---

# Container 容器

> [!WARNING]
> `Container` 已废弃,仅为兼容现有代码而保留。新布局请使用 [`Layout`](./layout.md)(`TrLayout`)。两者的 API 并非一一对应,迁移时请根据 Layout 的区域插槽、布局模式和浮层状态重新配置。

## 代码示例

### 基本示例
Expand Down
15 changes: 10 additions & 5 deletions packages/components/src/container/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { App } from 'vue'
import Container from './index.vue'
import ContainerComponent from './index.vue'

Container.name = 'TrContainer'
ContainerComponent.name = 'TrContainer'

const install = function <T>(app: App<T>) {
app.component(Container.name!, Container)
app.component(ContainerComponent.name!, ContainerComponent)
}

Container.install = install
ContainerComponent.install = install

export default Container as typeof Container & { install: typeof install }
/**
* @deprecated Container is kept for compatibility. Use `Layout` (`TrLayout`) for new layouts.
*/
const Container = ContainerComponent as typeof ContainerComponent & { install: typeof install }

export default Container
82 changes: 79 additions & 3 deletions packages/components/src/dropdown-menu/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { computed, nextTick, ref, watch, watchEffect } from 'vue'
import TrBasePopper from '../base-popper'
import { usePopperHover } from './composables/usePopperHover'
import { DropdownMenuEmits, DropdownMenuItem, DropdownMenuProps } from './index.type'
Expand Down Expand Up @@ -33,6 +33,14 @@ const basePopperRef = ref<InstanceType<typeof TrBasePopper> | null>(null)
const triggerRef = computed(() => basePopperRef.value?.triggerRef)
const dropdownMenuRef = computed(() => basePopperRef.value?.popperRef)

watchEffect(() => {
const trigger = triggerRef.value
if (!trigger) return

trigger.setAttribute('aria-haspopup', 'menu')
trigger.setAttribute('aria-expanded', String(Boolean(show.value)))
})

if (props.trigger === 'click' || props.trigger === 'manual') {
onClickOutside(
dropdownMenuRef,
Expand All @@ -56,9 +64,75 @@ const handleTriggerClick = () => {
}
}

const focusTrigger = async () => {
await nextTick()
if (!show.value) triggerRef.value?.focus()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const getMenuItems = () => {
return Array.from(dropdownMenuRef.value?.querySelectorAll<HTMLElement>('[role="menuitem"]') || [])
}

const focusMenuItem = async (position: 'first' | 'last') => {
await nextTick()
const items = getMenuItems()
items[position === 'first' ? 0 : items.length - 1]?.focus()
}

const handleTriggerKeydown = (event: KeyboardEvent) => {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
if (props.trigger === 'manual' && !show.value) return

event.preventDefault()
if (props.trigger !== 'manual') show.value = true
focusMenuItem(event.key === 'ArrowDown' ? 'first' : 'last')
} else if ((event.key === 'Enter' || event.key === ' ') && props.trigger !== 'manual') {
event.preventDefault()
show.value = true
focusMenuItem('first')
} else if (event.key === 'Escape' && show.value) {
event.preventDefault()
show.value = false
}
}

const handleMenuKeydown = (event: KeyboardEvent) => {
const items = getMenuItems()
const currentIndex = items.indexOf(document.activeElement as HTMLElement)

if (event.key === 'Tab') {
if (props.trigger !== 'manual') show.value = false
return
}

if (event.key === 'Escape') {
event.preventDefault()
show.value = false
focusTrigger()
return
}

if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
items[currentIndex]?.click()
return
}

if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) || items.length === 0) return

event.preventDefault()
let nextIndex = currentIndex
if (event.key === 'Home') nextIndex = 0
if (event.key === 'End') nextIndex = items.length - 1
if (event.key === 'ArrowDown') nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0
if (event.key === 'ArrowUp') nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1
items[nextIndex]?.focus()
}

const handleItemClick = (item: DropdownMenuItem) => {
show.value = false
emit('item-click', item)
focusTrigger()
}

defineExpose({
Expand All @@ -78,17 +152,19 @@ defineExpose({
:offset="8"
:transition-props="{ name: 'tr-dropdown-menu' }"
:prevent-overflow="true"
:trigger-events="{ onClick: handleTriggerClick }"
:trigger-events="{ onClick: handleTriggerClick, onKeydown: handleTriggerKeydown }"
>
<template #trigger>
<slot name="trigger" />
</template>
<template #content>
<ul class="tr-dropdown-menu__list">
<ul class="tr-dropdown-menu__list" role="menu" @keydown="handleMenuKeydown">
<li
class="tr-dropdown-menu__list-item"
v-for="item in props.items"
:key="item.id"
role="menuitem"
tabindex="-1"
@click="handleItemClick(item)"
>
{{ item.text }}
Expand Down
54 changes: 51 additions & 3 deletions packages/components/src/history/components/MenuList.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts" generic="T">
import { onClickOutside, useElementBounding, useElementSize, useWindowSize } from '@vueuse/core'
import { computed, CSSProperties, ref } from 'vue'
import { computed, CSSProperties, nextTick, ref } from 'vue'
import { toCssUnit } from '../../shared/utils'
import { HistoryMenuItem } from '../index.type'

Expand Down Expand Up @@ -60,11 +60,59 @@ const handleItemClick = (item: { id: string; text: string }) => {
trigger.value = null
data.value = null
}

const getMenuItems = () => Array.from(menuRef.value?.querySelectorAll<HTMLElement>('[role="menuitem"]') || [])

const focusFirstItem = () => getMenuItems()[0]?.focus()
const focusLastItem = () => getMenuItems().at(-1)?.focus()

const closeAndFocusTrigger = () => {
const triggerElement = trigger.value
trigger.value = null
data.value = null
nextTick(() => triggerElement?.focus())
}

const handleKeydown = (event: KeyboardEvent) => {
const items = getMenuItems()
const currentIndex = items.indexOf(document.activeElement as HTMLElement)

Comment thread
gene9831 marked this conversation as resolved.
if (event.key === 'Escape') {
event.preventDefault()
closeAndFocusTrigger()
return
}

if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
items[currentIndex]?.click()
return
}

if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) || items.length === 0) return

event.preventDefault()
let nextIndex = currentIndex
if (event.key === 'Home') nextIndex = 0
if (event.key === 'End') nextIndex = items.length - 1
if (event.key === 'ArrowDown') nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0
if (event.key === 'ArrowUp') nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1
items[nextIndex]?.focus()
}

defineExpose({ focusFirstItem, focusLastItem })
</script>

<template>
<ul class="tr-history__menu-list" ref="menuRef" :style="styles">
<li class="tr-history__menu-list__item" v-for="item in props.items" :key="item.id" @click="handleItemClick(item)">
<ul class="tr-history__menu-list" ref="menuRef" :style="styles" role="menu" @keydown="handleKeydown">
<li
class="tr-history__menu-list__item"
v-for="item in props.items"
:key="item.id"
role="menuitem"
tabindex="-1"
@click="handleItemClick(item)"
>
<component :is="item.icon" />
<span>{{ item.text }}</span>
</li>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { onClickOutside } from '@vueuse/core'
import { computed, nextTick, ref, type Ref } from 'vue'
import { computed, nextTick, ref, shallowRef, type Ref } from 'vue'

export interface UseRenameEditorProps<T extends { title: string }> {
renameControlOnClickOutside?: 'confirm' | 'cancel' | 'none'
Expand All @@ -21,7 +21,7 @@ export const useRenameEditor = <T extends { title: string }>({
renameControlOnClickOutside,
onItemTitleChange,
}: UseRenameEditorProps<T>): UseRenameEditorReturn<T> => {
const editingItem = ref<T | undefined>(undefined) as Ref<T | undefined>
const editingItem = shallowRef<T | undefined>(undefined) as Ref<T | undefined>
const editorRefList = ref<HTMLInputElement[] | null>(null)
const editorRef = computed(() => editorRefList.value?.at(0))
const editorConfirmRefList = ref<HTMLButtonElement[] | null>(null)
Expand Down
Loading
Loading