mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-05-20 15:14:57 +02:00
@@ -70,7 +70,24 @@
|
||||
// If current element is not found (-1), try to find a related element
|
||||
if (currentIndex === -1) {
|
||||
// Check if the current element is inside a TextFieldSelect or similar component
|
||||
const parentComponent = currentlyFocused?.closest('.relative');
|
||||
const parentComponent = currentlyFocused?.closest('.textfield-select-container');
|
||||
if (parentComponent) {
|
||||
// Look for the input element within the same component
|
||||
const inputInComponent = parentComponent.querySelector('input');
|
||||
if (inputInComponent) {
|
||||
const inputIndex = focusableElements.indexOf(inputInComponent);
|
||||
if (inputIndex !== -1) {
|
||||
// Use the input's position for navigation and continue tab flow
|
||||
currentIndex = inputIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still can't find the element, handle it gracefully
|
||||
if (currentIndex === -1) {
|
||||
// Check if the current element is inside a TextFieldSelect or similar component
|
||||
const parentComponent = currentlyFocused?.closest('.textfield-select-container');
|
||||
if (parentComponent) {
|
||||
// Look for the input element within the same component
|
||||
const inputInComponent = parentComponent.querySelector('input');
|
||||
@@ -141,23 +158,70 @@
|
||||
|
||||
const elements = modalElement.querySelectorAll(focusableSelectors.join(', '));
|
||||
return Array.from(elements).filter((el) => {
|
||||
// Exclude dropdown option buttons from TextFieldSelect components
|
||||
// exclude dropdown option buttons from TextFieldSelect components
|
||||
if (el.role === 'option' && el.closest('[role="listbox"]')) {
|
||||
return false;
|
||||
}
|
||||
return el.offsetWidth > 0 && el.offsetHeight > 0 && !el.hasAttribute('hidden');
|
||||
|
||||
// check if element is truly visible (has dimensions and not hidden)
|
||||
if (el.offsetWidth === 0 && el.offsetHeight === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for hidden attribute
|
||||
if (el.hasAttribute('hidden')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check computed styles for visibility
|
||||
const style = window.getComputedStyle(el);
|
||||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if any parent container is hidden (for multi-step forms)
|
||||
let parent = el.parentElement;
|
||||
while (parent && parent !== modalElement) {
|
||||
const parentStyle = window.getComputedStyle(parent);
|
||||
if (parentStyle.display === 'none' || parentStyle.visibility === 'hidden') {
|
||||
return false;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const updateFocusableElements = () => {
|
||||
focusableElements = getFocusableElements();
|
||||
|
||||
// Exclude the close button from being the first focusable element
|
||||
// Reorder elements: form controls first, then navigation buttons, then close button
|
||||
const formElements = [];
|
||||
const navigationButtons = [];
|
||||
const closeButton = modalElement?.querySelector('[data-close-button]');
|
||||
if (closeButton && focusableElements.length > 1) {
|
||||
focusableElements = focusableElements.filter((el) => el !== closeButton);
|
||||
focusableElements.push(closeButton); // Add close button to the end
|
||||
|
||||
focusableElements.forEach((el) => {
|
||||
if (el === closeButton) {
|
||||
// close button goes last
|
||||
return;
|
||||
} else if (
|
||||
el instanceof HTMLButtonElement &&
|
||||
(el.textContent?.trim() === 'Next' || el.textContent?.trim() === 'Previous')
|
||||
) {
|
||||
navigationButtons.push(el);
|
||||
} else {
|
||||
// all other elements (form fields, form control buttons, etc.) go first
|
||||
formElements.push(el);
|
||||
}
|
||||
});
|
||||
|
||||
// Rebuild focusable elements in desired order: form controls, then navigation, then close
|
||||
focusableElements = [...formElements, ...navigationButtons];
|
||||
if (closeButton) {
|
||||
focusableElements.push(closeButton);
|
||||
}
|
||||
|
||||
firstFocusableElement = focusableElements[0] || null;
|
||||
lastFocusableElement = focusableElements[focusableElements.length - 1] || null;
|
||||
};
|
||||
@@ -186,20 +250,11 @@
|
||||
|
||||
// Exposed function to reset tab focus when modal content changes
|
||||
const handleResetTabFocus = async () => {
|
||||
// Only reset focus if no element is currently focused or if focus is outside modal
|
||||
const currentFocused = document.activeElement;
|
||||
const isInModal = modalElement?.contains(currentFocused);
|
||||
|
||||
// Check if a TextFieldSelect is currently selecting an option
|
||||
const isTextFieldSelectActive = modalElement?.querySelector('[data-selecting="true"]');
|
||||
|
||||
await tick();
|
||||
updateFocusableElements();
|
||||
|
||||
// Only focus first element if focus is not properly within the modal and no selection is happening
|
||||
if (!isInModal && !isTextFieldSelectActive && firstFocusableElement) {
|
||||
firstFocusableElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Call resetTabFocus when it changes
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
let showDropdown = false;
|
||||
let inputElement;
|
||||
let dropdownElement;
|
||||
let justSelected = false;
|
||||
|
||||
// Simple function to filter options based on input
|
||||
const filterOptions = (searchValue) => {
|
||||
@@ -45,9 +46,13 @@
|
||||
// Show dropdown when focused and there are options
|
||||
const handleFocus = () => {
|
||||
activeFormElement.set(id);
|
||||
showDropdown = true;
|
||||
hasTyped = false; // Reset typing flag to show all options
|
||||
allOptions = [...optionsArray]; // Show all options on focus
|
||||
// Don't open dropdown if we just selected something
|
||||
if (!justSelected) {
|
||||
showDropdown = true;
|
||||
hasTyped = false; // Reset typing flag to show all options
|
||||
allOptions = [...optionsArray]; // Show all options on focus
|
||||
}
|
||||
justSelected = false; // Reset the flag
|
||||
};
|
||||
|
||||
// Handle input changes for filtering
|
||||
@@ -63,7 +68,14 @@
|
||||
value = option;
|
||||
showDropdown = false;
|
||||
hasTyped = false; // Reset typing flag after selection
|
||||
justSelected = true; // Set flag to prevent dropdown reopening
|
||||
onSelect(option);
|
||||
// Focus the input field after selection without reopening dropdown
|
||||
setTimeout(() => {
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Close dropdown
|
||||
|
||||
@@ -259,6 +259,33 @@
|
||||
</div>
|
||||
</label>
|
||||
<div class="relative">
|
||||
{#if !isFocused}
|
||||
<div class="absolute left-1 top-1/2 transform -translate-y-1/2 flex bg-transparent z-10">
|
||||
<button
|
||||
type="button"
|
||||
class="w-8 h-8 text-base transition-all duration-200 rounded-lg border flex items-center justify-center {type ===
|
||||
'page'
|
||||
? 'border-green-500 dark:border-green-400 bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||
: 'border-gray-200 dark:border-gray-600 bg-grayblue-light dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:border-blue-300 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/30'}"
|
||||
on:click={() => handleTypeChange('page')}
|
||||
title="Page"
|
||||
>
|
||||
📄
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-8 h-8 text-base transition-all duration-200 rounded-lg border flex items-center justify-center ml-0.5 {type ===
|
||||
'proxy'
|
||||
? 'border-green-500 dark:border-green-400 bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||
: 'border-gray-200 dark:border-gray-600 bg-grayblue-light dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:border-blue-300 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/30'}"
|
||||
on:click={() => handleTypeChange('proxy')}
|
||||
title="Proxy"
|
||||
>
|
||||
<ProxySvgIcon size="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="flex items-center relative"
|
||||
class:w-28={size == 'small'}
|
||||
@@ -290,34 +317,6 @@
|
||||
{required}
|
||||
/>
|
||||
|
||||
<!-- Type selector buttons inside the input on the left - hidden when focused -->
|
||||
{#if !isFocused}
|
||||
<div class="absolute left-1 top-1/2 transform -translate-y-1/2 flex bg-transparent">
|
||||
<button
|
||||
type="button"
|
||||
class="w-8 h-8 text-base transition-all duration-200 rounded-lg border flex items-center justify-center {type ===
|
||||
'page'
|
||||
? 'border-green-500 dark:border-green-400 bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||
: 'border-gray-200 dark:border-gray-600 bg-grayblue-light dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:border-blue-300 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/30'}"
|
||||
on:click={() => handleTypeChange('page')}
|
||||
title="Page"
|
||||
>
|
||||
📄
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-8 h-8 text-base transition-all duration-200 rounded-lg border flex items-center justify-center ml-0.5 {type ===
|
||||
'proxy'
|
||||
? 'border-green-500 dark:border-green-400 bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||
: 'border-gray-200 dark:border-gray-600 bg-grayblue-light dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:border-blue-300 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/30'}"
|
||||
on:click={() => handleTypeChange('proxy')}
|
||||
title="Proxy"
|
||||
>
|
||||
<ProxySvgIcon size="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Search icon - visible when dropdown is open -->
|
||||
{#if showDropdown}
|
||||
<img
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { api } from '$lib/api/apiProxy.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { newTableURLParams } from '$lib/service/tableURLParams.js';
|
||||
@@ -315,15 +315,43 @@
|
||||
};
|
||||
});
|
||||
|
||||
const nextStep = () => {
|
||||
const nextStep = async () => {
|
||||
if (validateCurrentStep()) {
|
||||
currentStep = Math.min(currentStep + 1, campaignSteps.length);
|
||||
modalError = '';
|
||||
// reset tab focus after dom update - only for explicit step navigation
|
||||
await tick();
|
||||
// focus first element in current step
|
||||
setTimeout(() => {
|
||||
const currentStepContainer = document.querySelector(`#step-${currentStep}`);
|
||||
if (currentStepContainer) {
|
||||
const firstFocusable = currentStepContainer.querySelector(
|
||||
'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
if (firstFocusable && firstFocusable instanceof HTMLElement) {
|
||||
firstFocusable.focus();
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const previousStep = () => {
|
||||
const previousStep = async () => {
|
||||
currentStep = Math.max(currentStep - 1, 1);
|
||||
// reset tab focus after dom update - only for explicit step navigation
|
||||
await tick();
|
||||
// focus first element in current step
|
||||
setTimeout(() => {
|
||||
const currentStepContainer = document.querySelector(`#step-${currentStep}`);
|
||||
if (currentStepContainer) {
|
||||
const firstFocusable = currentStepContainer.querySelector(
|
||||
'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
if (firstFocusable && firstFocusable instanceof HTMLElement) {
|
||||
firstFocusable.focus();
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const validateCurrentStep = () => {
|
||||
@@ -1061,6 +1089,9 @@
|
||||
: 'bg-white dark:bg-gray-700 text-gray-500 dark:text-gray-300 border-2 border-gray-300 dark:border-gray-600'
|
||||
}
|
||||
`}
|
||||
role="tab"
|
||||
aria-selected={currentStep === index + 1}
|
||||
aria-label={`Step ${index + 1}: ${step.name}`}
|
||||
>
|
||||
{#if currentStep > index + 1}
|
||||
<svg
|
||||
|
||||
Reference in New Issue
Block a user