Enhance SkillTreeMultiSelect Component and CSS Styles
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 48s
Test Suite / playwright-tests (push) Successful in 1m28s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m16s
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 48s
Test Suite / playwright-tests (push) Successful in 1m28s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m16s
- Updated the SkillTreeMultiSelect component to support dynamic positioning and improved accessibility through the use of portals. - Refactored the dropdown panel rendering logic to enhance user experience when selecting skills. - Added CSS styles for the exercise filter modal to improve layout and responsiveness. - Introduced new styles for the skill tree multiselect panel, ensuring better visual integration and usability.
This commit is contained in:
parent
2d187447bb
commit
a7a428745f
|
|
@ -4086,6 +4086,12 @@ html.modal-scroll-locked .app-main {
|
|||
|
||||
.exercise-filter-modal.admin-modal-sheet {
|
||||
max-width: min(920px, calc(100dvw - 16px));
|
||||
max-height: min(92vh, 920px);
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.exercise-filter-modal.admin-modal-sheet {
|
||||
max-height: min(90vh, 920px);
|
||||
}
|
||||
}
|
||||
.exercise-filter-modal .admin-modal-sheet__body.exercise-filter-modal__scroll {
|
||||
flex: 1;
|
||||
|
|
@ -6418,6 +6424,24 @@ html.modal-scroll-locked .app-main {
|
|||
max-height: min(360px, 55vh);
|
||||
overflow: auto;
|
||||
}
|
||||
.skill-tree-multiselect__panel--portal {
|
||||
position: fixed;
|
||||
right: auto;
|
||||
top: auto;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.skill-tree-multiselect__panel--portal .multiselect-combo__list {
|
||||
position: static;
|
||||
left: auto;
|
||||
right: auto;
|
||||
top: auto;
|
||||
margin: 0;
|
||||
max-height: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.skill-tree {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { collectSkillLeavesFromTree, buildSkillCatalogTree } from '../utils/skillCatalogTree'
|
||||
import SkillTreePickerPanel from './SkillTreePickerPanel'
|
||||
|
||||
const PANEL_MAX_HEIGHT = 360
|
||||
const PANEL_MIN_HEIGHT = 140
|
||||
const PANEL_Z_INDEX = 1100
|
||||
|
||||
function normId(id) {
|
||||
return String(id)
|
||||
}
|
||||
|
|
@ -17,11 +22,15 @@ export default function SkillTreeMultiSelect({
|
|||
browseLabel = '▼ Katalog',
|
||||
emptyHint = 'Keine Treffer',
|
||||
className = '',
|
||||
usePortal = true,
|
||||
}) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [browseTree, setBrowseTree] = useState(false)
|
||||
const [panelStyle, setPanelStyle] = useState(null)
|
||||
const rootRef = useRef(null)
|
||||
const fieldRef = useRef(null)
|
||||
const panelRef = useRef(null)
|
||||
|
||||
const tree = useMemo(() => buildSkillCatalogTree(skills), [skills])
|
||||
const selectedSet = useMemo(() => new Set(value.map(normId)), [value])
|
||||
|
|
@ -56,17 +65,99 @@ export default function SkillTreeMultiSelect({
|
|||
|
||||
useEffect(() => {
|
||||
const onDoc = (e) => {
|
||||
if (!rootRef.current?.contains(e.target)) {
|
||||
setOpen(false)
|
||||
setBrowseTree(false)
|
||||
}
|
||||
const t = e.target
|
||||
if (rootRef.current?.contains(t) || panelRef.current?.contains(t)) return
|
||||
setOpen(false)
|
||||
setBrowseTree(false)
|
||||
}
|
||||
document.addEventListener('mousedown', onDoc)
|
||||
return () => document.removeEventListener('mousedown', onDoc)
|
||||
}, [])
|
||||
|
||||
const updatePanelPosition = useCallback(() => {
|
||||
const anchor = fieldRef.current
|
||||
if (!anchor) return
|
||||
const rect = anchor.getBoundingClientRect()
|
||||
const gap = 4
|
||||
const margin = 12
|
||||
const spaceBelow = window.innerHeight - rect.bottom - gap - margin
|
||||
const spaceAbove = rect.top - gap - margin
|
||||
const openUp = spaceBelow < PANEL_MIN_HEIGHT && spaceAbove > spaceBelow
|
||||
const available = Math.max(0, openUp ? spaceAbove : spaceBelow)
|
||||
const maxHeight = Math.min(PANEL_MAX_HEIGHT, Math.max(PANEL_MIN_HEIGHT, available))
|
||||
const top = openUp ? Math.max(margin, rect.top - gap - maxHeight) : rect.bottom + gap
|
||||
|
||||
setPanelStyle({
|
||||
position: 'fixed',
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
top,
|
||||
maxHeight,
|
||||
zIndex: PANEL_Z_INDEX,
|
||||
})
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open || !usePortal) {
|
||||
setPanelStyle(null)
|
||||
return undefined
|
||||
}
|
||||
updatePanelPosition()
|
||||
window.addEventListener('resize', updatePanelPosition)
|
||||
window.addEventListener('scroll', updatePanelPosition, true)
|
||||
return () => {
|
||||
window.removeEventListener('resize', updatePanelPosition)
|
||||
window.removeEventListener('scroll', updatePanelPosition, true)
|
||||
}
|
||||
}, [open, usePortal, updatePanelPosition, query, browseTree, value.length])
|
||||
|
||||
const showTree = browseTree || !query.trim()
|
||||
|
||||
const panelContent = showTree ? (
|
||||
<SkillTreePickerPanel
|
||||
skills={skills}
|
||||
excludeIds={value}
|
||||
searchQuery={query}
|
||||
onPickSkill={(id) => addId(id)}
|
||||
pickMode="multi"
|
||||
/>
|
||||
) : (
|
||||
<ul className="multiselect-combo__list" role="listbox">
|
||||
{leaves.filter((l) => l.pathLabel.toLowerCase().includes(query.trim().toLowerCase())).length === 0 ? (
|
||||
<li className="multiselect-combo__empty">{emptyHint}</li>
|
||||
) : (
|
||||
leaves
|
||||
.filter((l) => l.pathLabel.toLowerCase().includes(query.trim().toLowerCase()))
|
||||
.map((l) => (
|
||||
<li key={normId(l.id)}>
|
||||
<button
|
||||
type="button"
|
||||
className="multiselect-combo__opt skill-tree__pick--path"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => addId(l.id)}
|
||||
>
|
||||
<span className="skill-tree__pick-name">{l.label}</span>
|
||||
<span className="skill-tree__pick-path">{l.pathLabel}</span>
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
|
||||
const dropdownPanel =
|
||||
open && (usePortal ? panelStyle : true) ? (
|
||||
<div
|
||||
ref={panelRef}
|
||||
className={
|
||||
'skill-tree-multiselect__panel' + (usePortal ? ' skill-tree-multiselect__panel--portal' : '')
|
||||
}
|
||||
style={usePortal ? panelStyle : undefined}
|
||||
>
|
||||
{panelContent}
|
||||
</div>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className={`multiselect-combo skill-tree-multiselect ${className}`.trim()} ref={rootRef}>
|
||||
<div className="multiselect-combo__chips">
|
||||
|
|
@ -85,7 +176,7 @@ export default function SkillTreeMultiSelect({
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="multiselect-combo__field">
|
||||
<div className="multiselect-combo__field" ref={fieldRef}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input multiselect-combo__input"
|
||||
|
|
@ -113,42 +204,8 @@ export default function SkillTreeMultiSelect({
|
|||
{browseLabel}
|
||||
</button>
|
||||
</div>
|
||||
{open ? (
|
||||
<div className="skill-tree-multiselect__panel">
|
||||
{showTree ? (
|
||||
<SkillTreePickerPanel
|
||||
skills={skills}
|
||||
excludeIds={value}
|
||||
searchQuery={query}
|
||||
onPickSkill={(id) => addId(id)}
|
||||
pickMode="multi"
|
||||
/>
|
||||
) : (
|
||||
<ul className="multiselect-combo__list" role="listbox">
|
||||
{leaves.filter((l) => l.pathLabel.toLowerCase().includes(query.trim().toLowerCase())).length ===
|
||||
0 ? (
|
||||
<li className="multiselect-combo__empty">{emptyHint}</li>
|
||||
) : (
|
||||
leaves
|
||||
.filter((l) => l.pathLabel.toLowerCase().includes(query.trim().toLowerCase()))
|
||||
.map((l) => (
|
||||
<li key={normId(l.id)}>
|
||||
<button
|
||||
type="button"
|
||||
className="multiselect-combo__opt skill-tree__pick--path"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => addId(l.id)}
|
||||
>
|
||||
<span className="skill-tree__pick-name">{l.label}</span>
|
||||
<span className="skill-tree__pick-path">{l.pathLabel}</span>
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{!usePortal && dropdownPanel}
|
||||
{usePortal && dropdownPanel ? createPortal(dropdownPanel, document.body) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user