Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
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 @@ -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 {
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>
24 changes: 19 additions & 5 deletions resources/js/components/fieldtypes/bard/Set.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -49,7 +47,7 @@
<div class="flex items-center gap-2" v-if="!isReadOnly">
<Switch size="xs" v-model="enabled" 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 Expand Up @@ -120,6 +118,12 @@ import { reveal } from '@api';
export default {
props: nodeViewProps,

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

components: {
Button,
Dropdown,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -374,6 +387,7 @@ export default {
},

beforeUnmount() {
if (this._dropdownJustClosedTimeout) clearTimeout(this._dropdownJustClosedTimeout);
this._draggableObserver?.disconnect();
},
};
Expand Down
17 changes: 15 additions & 2 deletions resources/js/components/fieldtypes/replicator/Set.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
</script>

<template>
Expand All @@ -132,7 +144,8 @@ reveal.use(rootEl, () => emit('expanded'));
data-replicator-set
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
'border-red-500': hasError,
'st-dropdown-just-closed': dropdownJustClosed
}"
:data-collapsed="collapsed ?? undefined"
:data-error="hasError ?? undefined"
Expand Down Expand Up @@ -174,7 +187,7 @@ reveal.use(rootEl, () => emit('expanded'));
</button>
<div class="flex items-center gap-2" v-if="!readOnly">
<Switch size="xs" :model-value="enabled" @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
25 changes: 25 additions & 0 deletions resources/js/components/ui/Button/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ const tag = computed(() => {
});
const iconOnly = computed(() => (props.icon && !hasDefaultSlot && !props.text) || props.iconOnly);

function focusButtonOnPointerDown(event) {
if (props.disabled || props.loading) return;

const target = event.currentTarget;
if (!(target instanceof HTMLButtonElement)) return;

// Safari may not focus buttons on click, which breaks :focus-within-dependent UI states.
target.focus({ preventScroll: true });
}

function keepGroupButtonFocused(event) {
if (props.disabled || props.loading) return;

const target = event.currentTarget;
if (!(target instanceof HTMLButtonElement)) return;

if (!target.closest('[data-ui-button-group]')) return;

// Some component stacks re-focus a wrapper on mouseup/click. Re-assert the button focus.
requestAnimationFrame(() => target.focus({ preventScroll: true }));
setTimeout(() => target.focus({ preventScroll: true }), 0);
}

const buttonClasses = computed(() => {
const classes = cva({
base: 'relative inline-flex items-center justify-center whitespace-nowrap shrink-0 font-medium antialiased cursor-pointer no-underline disabled:text-gray-400 dark:disabled:text-gray-600 disabled:[&_svg]:opacity-30 disabled:cursor-not-allowed [&_svg]:shrink-0 [&_svg]:text-gray-925 [&_svg]:opacity-60 dark:[&_svg]:text-white',
Expand Down Expand Up @@ -122,6 +145,8 @@ const buttonClasses = computed(() => {
:href
:target
:type="props.href ? null : type"
@pointerdown="focusButtonOnPointerDown"
@click="keepGroupButtonFocused"
>
<Icon v-if="icon" :name="icon" />
<Icon v-if="loading" name="loading" :size />
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