Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
60 changes: 57 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,65 @@ 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) => {
if (event.key === 'Tab') {
trigger.value = null
data.value = null
return
}

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