Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1413c8a
Only remove Bard set when backspace key is hit
duncanmcclean Mar 10, 2026
fc5d0c8
Selection state - Simplify CSS by converting some long Tailwind classes
jaygeorge Mar 10, 2026
08a5245
Selection state - Remove extra border not needed because it's already…
jaygeorge Mar 10, 2026
59aeb64
Selection state - Handle the selection state better when the dropdown…
jaygeorge Mar 10, 2026
5af85a0
When the dropdown has just closed (open→closed), keep the outline hid…
jaygeorge Mar 10, 2026
701fc00
Merge remote-tracking branch 'origin/removing-bard-sets' into bard-fo…
jaygeorge Mar 11, 2026
d62835b
Selection state - Fix button focus states. Re-focus [data-ui-button-g…
jaygeorge Mar 24, 2026
45f0d12
Selection state - Fix button focus states. Re-focus quick actions to …
jaygeorge Mar 24, 2026
f66fba5
Merge branch '6.x' into bard-focus-improvements
jaygeorge Mar 24, 2026
bb46eaf
Revert conflict
jaygeorge Mar 24, 2026
4855c4f
Restore "When the dropdown has just closed (open→closed), keep the ou…
jaygeorge Mar 24, 2026
5c24fb5
Selection state - Further adjustments
jaygeorge Mar 24, 2026
280691d
Hide the selection outline when we have a modal open, since the focus…
jaygeorge Mar 24, 2026
e52a6dd
Reindent
jaygeorge Mar 24, 2026
9740b06
Hide the selection outline when we have a popper open, since the focu…
jaygeorge Mar 24, 2026
b511863
Correct selector
jaygeorge Mar 24, 2026
1a9efb9
Merge branch '6.x' into bard-focus-improvements
jaygeorge Mar 24, 2026
f1dcc5e
Copy set.js from v6
jaygeorge Mar 24, 2026
a8a8241
Merge branch '6.x' into bard-focus-improvements
jaygeorge May 26, 2026
b3ec0b5
Merge branch '6.x' into bard-focus-improvements
jaygeorge Oct 8, 2026
bc10801
Skip button focus helpers when the button is read-only
jaygeorge Oct 8, 2026
7453ed2
Move button focus fixes from Button into the Bard set
jaygeorge Oct 8, 2026
71cb436
Stop the selection outline flickering in Safari when clicking buttons
jaygeorge Oct 8, 2026
e1ef2ad
Merge 6.x into bard-focus-improvements
jackmcdade Oct 13, 2026
e46a425
Merge branch '6.x' into bard-focus-improvements
jackmcdade Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions resources/css/components/fieldtypes/bard.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,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 {
Expand Down
2 changes: 1 addition & 1 deletion resources/css/elements/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
</button>
</div>

s*/
*/
.show-focus-within:has(.show-focus-within_target:focus-visible) {
/* Stop the transition ruining the focus outline quickly appearing. */
transition: none;
Expand Down
24 changes: 21 additions & 3 deletions resources/js/components/field-actions/FieldActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@
</DropdownMenu>
</Dropdown>
<ButtonGroup class="mr-0.75 -mt-0.5">
<!-- Keep quick actions focusable for :focus-within styles, even when disabled. -->
<Button
v-for="(action, index) in actions.filter((a) => a.quick)"
:key="index"
@click="action.run()"
@click="runQuickAction(action, $event)"
v-tooltip="action.title"
size="2xs"
:disabled="action.disabled"
:aria-disabled="action.disabled ? 'true' : null"
:class="{ '!cursor-not-allowed': action.disabled }"
:icon-only="true"
:aria-label="action.title"
>
<ui-icon :name="action.icon" class="size-3.5" />
<ui-icon :name="action.icon" class="size-3.5" :class="{ '!opacity-30': action.disabled }" />
</Button>
</ButtonGroup>
</div>
Expand Down Expand Up @@ -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();
},
},
};
</script>
71 changes: 65 additions & 6 deletions resources/js/components/fieldtypes/bard/Set.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@
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"
:dir="uiDirection"
: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"
@copy.stop
@paste.stop
@cut.stop
@dragstart="preventNodeSelectionDrag"
@mousedown="preventFormControlNodeSelection"
@mousedown="onMousedown"
@click="keepButtonGroupFocused"
>
<div ref="content" hidden />
<header
Expand Down Expand Up @@ -58,7 +57,7 @@
v-tooltip="enabled ? __('Included in output') : __('Hidden from output')"
/>

<Dropdown>
<Dropdown @closed="onSetDropdownClosed">
<template #trigger>
<Button icon="dots" variant="ghost" size="xs" :aria-label="__('Open dropdown menu')" @mousedown.prevent />
</template>
Expand Down Expand Up @@ -137,6 +136,12 @@ export default {
};
},

data() {
return {
dropdownJustClosed: false,
};
},

components: {
Button,
Dropdown,
Expand Down Expand Up @@ -360,6 +365,59 @@ export default {
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);
},

onMousedown(event) {
this.focusClickedButton(event);
this.preventFormControlNodeSelection(event);
},

clickedButton(event) {
const target = event.target instanceof Element ? event.target : event.target.parentElement;

return target?.closest('button:not(:disabled)');
},

focusClickedButton(event) {
// Safari doesn't focus buttons on click, which breaks the :focus-within rules for the selection outline.
// Buttons that keep focus elsewhere on purpose (e.g. with @mousedown.prevent) are left alone.
if (event.defaultPrevented) return;

const button = this.clickedButton(event);
if (!button) return;

button.focus({ preventScroll: true });

// Safari's default mousedown action then moves focus off the button again, which makes
// the outline flicker until it's re-focused. Preventing it keeps focus where we put it.
if (document.activeElement === button) event.preventDefault();
},

keepButtonGroupFocused(event) {
const button = this.clickedButton(event);
if (!button?.closest('[data-ui-button-group]')) return;

// Some components re-focus a wrapper after a click, so put focus back on the button.
// If the click moved focus somewhere else on purpose (e.g. into a modal), leave it there.
const refocus = () => {
const active = document.activeElement;
if (active === button) return;
if (active && active !== document.body && !active.contains(button)) return;

button.focus({ preventScroll: true });
};

requestAnimationFrame(refocus);
setTimeout(refocus, 0);
},

preventFormControlNodeSelection(event) {
const target = event.target instanceof Element ? event.target : event.target.parentElement;

Expand Down Expand Up @@ -411,6 +469,7 @@ export default {
},

beforeUnmount() {
if (this._dropdownJustClosedTimeout) clearTimeout(this._dropdownJustClosedTimeout);
this._draggableObserver?.disconnect();
},
};
Expand Down
15 changes: 14 additions & 1 deletion resources/js/components/fieldtypes/replicator/Set.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,18 @@ function startMovingFromMenu() {

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);
}
</script>

<template>
Expand All @@ -167,6 +179,7 @@ reveal.use(rootEl, () => emit('expanded'));
class="relative w-full rounded-lg border border-gray-300 text-base dark:border-white/10 bg-white dark:bg-gray-900 dark:inset-shadow-2xs dark:inset-shadow-black shadow-ui-sm dark:[&_[data-ui-switch]]:border-gray-600 dark:[&_[data-ui-switch]]:border-1"
:class="{
'border-red-500': hasError,
'st-dropdown-just-closed': dropdownJustClosed,
'border-dashed border-gray-300 dark:border-gray-600': readOnly && !hasError,
'border-dashed': readOnly && hasError,
'focus-outline': moving,
Expand Down Expand Up @@ -227,7 +240,7 @@ reveal.use(rootEl, () => emit('expanded'));
@update:model-value="toggleEnabledState"
v-tooltip="enabled ? __('Included in output') : __('Hidden from output')"
/>
<Dropdown>
<Dropdown @closed="onSetDropdownClosed">
<template #trigger>
<Button icon="dots" variant="ghost" size="xs" :aria-label="__('Open dropdown menu')" />
</template>
Expand Down
10 changes: 9 additions & 1 deletion resources/js/components/ui/Dropdown/Dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ defineOptions({

const attrs = useAttrs();

const emit = defineEmits(['closed']);

const props = defineProps({
/** The preferred alignment against the trigger. May change when collisions occur. <br><br> Options: `start`, `center`, `end` */
align: { type: String, default: 'start' },
Expand All @@ -19,6 +21,12 @@ const props = defineProps({
side: { type: String, default: 'bottom' },
});

function onOpenChange(open) {
if (open === false) {
emit('closed');
}
}

const dropdownContentClasses = cva({
base: [
'rounded-xl min-w-64 bg-gray-50 dark:bg-gray-800 outline-hidden overflow-y-auto group z-50',
Expand All @@ -29,7 +37,7 @@ const dropdownContentClasses = cva({
</script>

<template>
<DropdownMenuRoot>
<DropdownMenuRoot @update:open="onOpenChange">
<DropdownMenuTrigger as-child data-ui-dropdown-trigger>
<slot name="trigger">
<Button icon="dots" variant="ghost" size="sm" v-bind="attrs" :aria-label="__('Open dropdown menu')" />
Expand Down
Loading