mirror of
https://github.com/mue/mue.git
synced 2026-06-08 14:10:42 +02:00
feat(Dropdown): implement closing animation and portal rendering for dropdown menu
fix(QuoteOptions): ensure authorDetails is set to true for users upgrading from older versions
This commit is contained in:
8
.github/workflows/beta-release.yml
vendored
8
.github/workflows/beta-release.yml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- beta
|
- beta
|
||||||
tags:
|
tags:
|
||||||
- 'v*-beta.*'
|
- "v*-beta.*"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: '1.3.1'
|
bun-version: "1.3.1"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
- name: Create or Update GitHub Pre-Release
|
- name: Create or Update GitHub Pre-Release
|
||||||
run: |
|
run: |
|
||||||
RELEASE_NOTES=$(cat <<EOF
|
RELEASE_NOTES=$(cat <<EOF
|
||||||
## 🧪 Mue Beta v${{ steps.version.outputs.version }}
|
## 🧪 Mue v${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
**⚠️ This is a beta release for testing purposes only.**
|
**⚠️ This is a beta release for testing purposes only.**
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ jobs:
|
|||||||
gh release create "v${{ steps.version.outputs.version }}" \
|
gh release create "v${{ steps.version.outputs.version }}" \
|
||||||
"build/chrome-${{ steps.version.outputs.version }}.zip" \
|
"build/chrome-${{ steps.version.outputs.version }}.zip" \
|
||||||
"build/firefox-${{ steps.version.outputs.version }}.zip" \
|
"build/firefox-${{ steps.version.outputs.version }}.zip" \
|
||||||
--title "Beta v${{ steps.version.outputs.version }}" \
|
--title "v${{ steps.version.outputs.version }}" \
|
||||||
--notes "$RELEASE_NOTES" \
|
--notes "$RELEASE_NOTES" \
|
||||||
--prerelease
|
--prerelease
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import variables from 'config/variables';
|
import variables from 'config/variables';
|
||||||
import { memo, useState, useCallback, useRef, useEffect } from 'react';
|
import { memo, useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { MdExpandMore, MdCheck, MdRefresh } from 'react-icons/md';
|
import { MdExpandMore, MdCheck, MdRefresh } from 'react-icons/md';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -8,25 +9,51 @@ import EventBus from 'utils/eventbus';
|
|||||||
import './Dropdown.scss';
|
import './Dropdown.scss';
|
||||||
|
|
||||||
const Dropdown = memo((props) => {
|
const Dropdown = memo((props) => {
|
||||||
const [value, setValue] = useState(
|
const [value, setValue] = useState(localStorage.getItem(props.name) || props.items[0]?.value);
|
||||||
localStorage.getItem(props.name) || props.items[0]?.value,
|
|
||||||
);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||||
|
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
|
const controlRef = useRef(null);
|
||||||
|
const menuRef = useRef(null);
|
||||||
const optionsRef = useRef([]);
|
const optionsRef = useRef([]);
|
||||||
|
|
||||||
|
const closeDropdown = useCallback(() => {
|
||||||
|
setIsClosing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setIsClosing(false);
|
||||||
|
setFocusedIndex(-1);
|
||||||
|
}, 200); // Match animation duration
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
if (
|
||||||
setIsOpen(false);
|
containerRef.current &&
|
||||||
setFocusedIndex(-1);
|
!containerRef.current.contains(event.target) &&
|
||||||
|
menuRef.current &&
|
||||||
|
!menuRef.current.contains(event.target)
|
||||||
|
) {
|
||||||
|
closeDropdown();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}, [closeDropdown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && controlRef.current) {
|
||||||
|
const rect = controlRef.current.getBoundingClientRect();
|
||||||
|
setMenuPosition({
|
||||||
|
top: rect.bottom + 4,
|
||||||
|
left: rect.left,
|
||||||
|
width: rect.width,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
@@ -37,8 +64,7 @@ const Dropdown = memo((props) => {
|
|||||||
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
|
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
|
||||||
|
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
setIsOpen(false);
|
closeDropdown();
|
||||||
setFocusedIndex(-1);
|
|
||||||
|
|
||||||
if (!props.noSetting) {
|
if (!props.noSetting) {
|
||||||
localStorage.setItem(props.name, newValue);
|
localStorage.setItem(props.name, newValue);
|
||||||
@@ -69,18 +95,23 @@ const Dropdown = memo((props) => {
|
|||||||
case 'Enter':
|
case 'Enter':
|
||||||
case ' ':
|
case ' ':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsOpen(!isOpen);
|
if (isOpen) {
|
||||||
|
closeDropdown();
|
||||||
|
} else {
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
setIsOpen(false);
|
closeDropdown();
|
||||||
setFocusedIndex(-1);
|
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
} else {
|
} else {
|
||||||
setFocusedIndex((prev) => (prev < props.items.filter((i) => i !== null).length - 1 ? prev + 1 : prev));
|
setFocusedIndex((prev) =>
|
||||||
|
prev < props.items.filter((i) => i !== null).length - 1 ? prev + 1 : prev,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
@@ -126,8 +157,16 @@ const Dropdown = memo((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
ref={controlRef}
|
||||||
className="dropdown-control"
|
className="dropdown-control"
|
||||||
onClick={() => !props.disabled && setIsOpen(!isOpen)}
|
onClick={() => {
|
||||||
|
if (props.disabled) return;
|
||||||
|
if (isOpen) {
|
||||||
|
closeDropdown();
|
||||||
|
} else {
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
role="button"
|
role="button"
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
@@ -138,27 +177,39 @@ const Dropdown = memo((props) => {
|
|||||||
<span className="dropdown-value">{selectedItem?.text || value}</span>
|
<span className="dropdown-value">{selectedItem?.text || value}</span>
|
||||||
<MdExpandMore className={`dropdown-arrow ${isOpen ? 'open' : ''}`} />
|
<MdExpandMore className={`dropdown-arrow ${isOpen ? 'open' : ''}`} />
|
||||||
</div>
|
</div>
|
||||||
{isOpen && (
|
{(isOpen || isClosing) &&
|
||||||
<div className="dropdown-menu" role="listbox">
|
createPortal(
|
||||||
{props.items.map((item, index) =>
|
<div
|
||||||
item !== null ? (
|
ref={menuRef}
|
||||||
<div
|
className={`dropdown-menu ${isClosing ? 'closing' : ''}`}
|
||||||
key={id + item.value}
|
role="listbox"
|
||||||
ref={(el) => (optionsRef.current[index] = el)}
|
style={{
|
||||||
className={`dropdown-option ${value === item.value ? 'selected' : ''} ${index === focusedIndex ? 'focused' : ''}`}
|
position: 'fixed',
|
||||||
onClick={() => onChange(item.value)}
|
top: `${menuPosition.top}px`,
|
||||||
onKeyDown={(e) => handleOptionKeyDown(e, item)}
|
left: `${menuPosition.left}px`,
|
||||||
role="option"
|
width: `${menuPosition.width}px`,
|
||||||
aria-selected={value === item.value}
|
}}
|
||||||
tabIndex={0}
|
>
|
||||||
>
|
{props.items.map((item, index) =>
|
||||||
<span className="dropdown-option-text">{item.text}</span>
|
item !== null ? (
|
||||||
{value === item.value && <MdCheck className="dropdown-option-check" />}
|
<div
|
||||||
</div>
|
key={id + item.value}
|
||||||
) : null,
|
ref={(el) => (optionsRef.current[index] = el)}
|
||||||
)}
|
className={`dropdown-option ${value === item.value ? 'selected' : ''} ${index === focusedIndex ? 'focused' : ''}`}
|
||||||
</div>
|
onClick={() => onChange(item.value)}
|
||||||
)}
|
onKeyDown={(e) => handleOptionKeyDown(e, item)}
|
||||||
|
role="option"
|
||||||
|
aria-selected={value === item.value}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span className="dropdown-option-text">{item.text}</span>
|
||||||
|
{value === item.value && <MdCheck className="dropdown-option-check" />}
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
|
|||||||
}
|
}
|
||||||
return type;
|
return type;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Migration: Force authorDetails on for users upgrading from older versions
|
||||||
|
useState(() => {
|
||||||
|
if (localStorage.getItem('authorDetails') === null) {
|
||||||
|
localStorage.setItem('authorDetails', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const [customQuote, setCustomQuote] = useState(getCustom());
|
const [customQuote, setCustomQuote] = useState(getCustom());
|
||||||
|
|
||||||
const handleCustomQuote = (e, text, index, type) => {
|
const handleCustomQuote = (e, text, index, type) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user