|
23 | 23 | * @tagClick="onTagClick" /> |
24 | 24 | */ |
25 | 25 |
|
| 26 | +import type { Ref } from "vue"; |
| 27 | +
|
26 | 28 | import type { AnyHistoryEntry, MyHistory } from "@/api/histories"; |
27 | 29 | import { isMyHistory } from "@/api/histories"; |
28 | 30 |
|
@@ -76,13 +78,36 @@ interface Props { |
76 | 78 | * @default [] |
77 | 79 | */ |
78 | 80 | selectedHistoryIds?: { id: string }[]; |
| 81 | +
|
| 82 | + /** |
| 83 | + * Whether cards are clickable for navigation |
| 84 | + * @type {boolean} |
| 85 | + * @default false |
| 86 | + */ |
| 87 | + clickable?: boolean; |
| 88 | +
|
| 89 | + /** |
| 90 | + * Item refs for keyboard navigation |
| 91 | + * @type {Record<string, Ref<InstanceType<typeof HistoryCard> | null>>} |
| 92 | + * @default {} |
| 93 | + */ |
| 94 | + itemRefs?: Record<string, Ref<InstanceType<typeof HistoryCard> | null>>; |
| 95 | +
|
| 96 | + /** |
| 97 | + * Range select anchor for keyboard navigation |
| 98 | + * @type {AnyHistoryEntry | undefined} |
| 99 | + */ |
| 100 | + rangeSelectAnchor?: AnyHistoryEntry; |
79 | 101 | } |
80 | 102 |
|
81 | 103 | const props = withDefaults(defineProps<Props>(), { |
82 | 104 | gridView: false, |
83 | 105 | publishedView: false, |
84 | 106 | selectable: false, |
85 | 107 | selectedHistoryIds: () => [], |
| 108 | + clickable: false, |
| 109 | + itemRefs: () => ({}), |
| 110 | + rangeSelectAnchor: undefined, |
86 | 111 | }); |
87 | 112 |
|
88 | 113 | /** |
@@ -112,30 +137,59 @@ const emit = defineEmits<{ |
112 | 137 | * @event updateFilter |
113 | 138 | */ |
114 | 139 | (e: "updateFilter", key: string, value: any): void; |
| 140 | +
|
| 141 | + /** |
| 142 | + * Emitted when a keyboard event occurs on a history card |
| 143 | + * @event on-key-down |
| 144 | + */ |
| 145 | + (e: "on-key-down", history: AnyHistoryEntry, event: KeyboardEvent): void; |
| 146 | +
|
| 147 | + /** |
| 148 | + * Emitted when a history card is clicked |
| 149 | + * @event on-history-card-click |
| 150 | + */ |
| 151 | + (e: "on-history-card-click", history: AnyHistoryEntry, event: Event): void; |
115 | 152 | }>(); |
116 | 153 | </script> |
117 | 154 |
|
118 | 155 | <template> |
119 | | - <div class="history-card-list d-flex flex-wrap overflow-auto"> |
| 156 | + <div class="history-card-list d-flex flex-wrap overflow-auto pt-1"> |
120 | 157 | <HistoryCard |
121 | 158 | v-for="history in props.histories" |
| 159 | + :ref="props.itemRefs[history.id]" |
122 | 160 | :key="history.id" |
| 161 | + tabindex="0" |
123 | 162 | :history="history" |
124 | 163 | :grid-view="props.gridView" |
125 | 164 | :shared-view="props.sharedView" |
126 | 165 | :published-view="props.publishedView" |
127 | 166 | :archived-view="props.archivedView" |
128 | 167 | :selectable="props.selectable" |
129 | 168 | :selected="props.selectedHistoryIds.some((selected) => selected.id === history.id)" |
| 169 | + :clickable="props.clickable" |
| 170 | + class="history-card-in-list" |
| 171 | + :class="{ 'range-select-anchor-history': props.rangeSelectAnchor?.id === history.id }" |
130 | 172 | @select="isMyHistory(history) && emit('select', history)" |
131 | 173 | @tagClick="(...args) => emit('tagClick', ...args)" |
132 | 174 | @refreshList="(...args) => emit('refreshList', ...args)" |
133 | | - @updateFilter="(...args) => emit('updateFilter', ...args)" /> |
| 175 | + @updateFilter="(...args) => emit('updateFilter', ...args)" |
| 176 | + @on-key-down="(...args) => emit('on-key-down', ...args)" |
| 177 | + @on-history-card-click="(...args) => emit('on-history-card-click', ...args)" /> |
134 | 178 | </div> |
135 | 179 | </template> |
136 | 180 |
|
137 | 181 | <style lang="scss" scoped> |
| 182 | +@import "theme/blue.scss"; |
| 183 | +
|
138 | 184 | .history-card-list { |
139 | 185 | container: cards-list / inline-size; |
| 186 | +
|
| 187 | + .history-card-in-list { |
| 188 | + &.range-select-anchor-history { |
| 189 | + &:deep(.g-card-content) { |
| 190 | + box-shadow: 0 0 0 0.2rem transparentize($brand-primary, 0.75); |
| 191 | + } |
| 192 | + } |
| 193 | + } |
140 | 194 | } |
141 | 195 | </style> |
0 commit comments