diff --git a/resources/css/components/fieldtypes/bard.css b/resources/css/components/fieldtypes/bard.css index 89cdf7e52f5..bf8d95463f6 100644 --- a/resources/css/components/fieldtypes/bard.css +++ b/resources/css/components/fieldtypes/bard.css @@ -38,6 +38,38 @@ } } } +/* BARD / SELECTION STATE +=================================================== */ +@layer ui { + /* Hide the selection outline when we have a modal or combobox open, since the focus outline will no longer be correct. */ + body:not(:has([data-ui-modal-content], [data-ui-combobox-content])) { + /* + Here we're carefully controlling the “selection outline” when a set is selected with the mouse or keyboard. + Style the selection outline... + */ + .st-set-is-selected:not(:has( + /* Except if... */ + /* Any element inside the Set has focus (e.g. when editing inside a Bard field). This prevents the outer selection outline from showing while the user is actively working inside the Set. */ + :focus-within, + /* When the options dropdown is open on an _inner_ set, the outer selection outline should be hidden. Instead we'll show a focus outline just for the replicator set that's selected. */ + [data-ui-dropdown-trigger][data-state="open"], + /* When the dropdown has just closed (open → closed), keep the outline hidden briefly so it doesn't flicker back to the parent while focus settles. */ + .st-dropdown-just-closed + )), + /* Show a focus outline _just_ on the outer set, when we have the dropdown open on the outer set */ + .st-set-is-selected:has(> header [data-ui-dropdown-trigger][data-state="open"]), + /* Show a focus outline just for the replicator set that's selected. We're using a direct descendant selector here in case there are further nested replicator sets. */ + [data-replicator-set]:has(> header [data-ui-dropdown-trigger][data-state="open"]) { + &::before { + content: ''; + position: absolute; + inset: -1px; + pointer-events: none; + @apply border-2 border-blue-400 dark:border-blue-400 rounded-lg; + } + } + } +} /* BARD / FOOTER TOOLBAR =================================================== */ @layer ui { diff --git a/resources/css/elements/base.css b/resources/css/elements/base.css index 465bf07665f..b6d33381399 100644 --- a/resources/css/elements/base.css +++ b/resources/css/elements/base.css @@ -92,7 +92,7 @@ - s*/ + */ .show-focus-within:has(.show-focus-within_target:focus-visible) { /* Stop the transition ruining the focus outline quickly appearing. */ transition: none; diff --git a/resources/js/components/field-actions/FieldActions.vue b/resources/js/components/field-actions/FieldActions.vue index 53d7bb35085..d70afa01b1a 100644 --- a/resources/js/components/field-actions/FieldActions.vue +++ b/resources/js/components/field-actions/FieldActions.vue @@ -16,17 +16,19 @@ + @@ -59,5 +61,21 @@ export default { return this.actions.filter((a) => !a.quick).length > 0; }, }, + + methods: { + runQuickAction(action, event) { + const target = event?.currentTarget; + + if (action.disabled) { + if (target instanceof HTMLButtonElement) { + // Re-focus after click so selection/focus styling doesn't jump to parent containers. + requestAnimationFrame(() => target.focus({ preventScroll: true })); + } + return; + } + + action.run(); + }, + }, }; diff --git a/resources/js/components/fieldtypes/bard/Set.vue b/resources/js/components/fieldtypes/bard/Set.vue index 50c14230375..5afce6aefac 100644 --- a/resources/js/components/fieldtypes/bard/Set.vue +++ b/resources/js/components/fieldtypes/bard/Set.vue @@ -4,11 +4,9 @@ ref="container" class="shadow-ui-sm relative w-full rounded-lg border border-gray-300 bg-white text-base dark:border-white/10 dark:bg-gray-900 dark:inset-shadow-2xs dark:inset-shadow-black" :class="{ - // We’re styling a Set so that it shows a “selection outline” when selected with the mouse or keyboard. - // The extra `&:not(:has(:focus-within))` rule turns that outline off if any element inside the Set has focus (e.g. when editing inside a Bard field). - // This prevents the outer selection outline from showing while the user is actively working inside the Set. - 'st-set-is-selected [&:not(:has(:focus-within))]:border-blue-400! [&:not(:has(:focus-within))]:dark:border-blue-400! [&:not(:has(:focus-within))]:before:content-[\'\'] [&:not(:has(:focus-within))]:before:absolute [&:not(:has(:focus-within))]:before:inset-[-1px] [&:not(:has(:focus-within))]:before:pointer-events-none [&:not(:has(:focus-within))]:before:border-2 [&:not(:has(:focus-within))]:before:border-blue-400 [&:not(:has(:focus-within))]:dark:before:border-blue-400 [&:not(:has(:focus-within))]:before:rounded-lg': showSelectionHighlight, + 'st-set-is-selected': showSelectionHighlight, 'border-red-500': hasError, + 'st-dropdown-just-closed': dropdownJustClosed, }" :data-type="config.handle" contenteditable="false" @@ -49,7 +47,7 @@
- + @@ -120,6 +118,12 @@ import { reveal } from '@api'; export default { props: nodeViewProps, + data() { + return { + dropdownJustClosed: false, + }; + }, + components: { Button, Dropdown, @@ -342,6 +346,15 @@ export default { this.$el.setAttribute('draggable', false); this._draggableObserver?.observe(this.$el, { attributes: true, attributeFilter: ['draggable'] }); }, + + onSetDropdownClosed() { + this.dropdownJustClosed = true; + if (this._dropdownJustClosedTimeout) clearTimeout(this._dropdownJustClosedTimeout); + this._dropdownJustClosedTimeout = setTimeout(() => { + this.dropdownJustClosed = false; + this._dropdownJustClosedTimeout = null; + }, 150); + }, }, mounted() { @@ -374,6 +387,7 @@ export default { }, beforeUnmount() { + if (this._dropdownJustClosedTimeout) clearTimeout(this._dropdownJustClosedTimeout); this._draggableObserver?.disconnect(); }, }; diff --git a/resources/js/components/fieldtypes/replicator/Set.vue b/resources/js/components/fieldtypes/replicator/Set.vue index a906beaa8ef..b175eb1515f 100644 --- a/resources/js/components/fieldtypes/replicator/Set.vue +++ b/resources/js/components/fieldtypes/replicator/Set.vue @@ -122,6 +122,18 @@ function destroy() { const rootEl = ref(); reveal.use(rootEl, () => emit('expanded')); + +const dropdownJustClosed = ref(false); +let dropdownJustClosedTimeout = null; + +function onSetDropdownClosed() { + dropdownJustClosed.value = true; + if (dropdownJustClosedTimeout) clearTimeout(dropdownJustClosedTimeout); + dropdownJustClosedTimeout = setTimeout(() => { + dropdownJustClosed.value = false; + dropdownJustClosedTimeout = null; + }, 150); +}