Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: use stacks in useEscapeKeydown & introduce escapeBehavior prop #1142

Merged
merged 40 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift click to select a range
fd42c1b
fix(escape-keydown): using a stack for correctly handling nested elem…
anatolzak Mar 30, 2024
8e3b2a5
update docs description
anatolzak Mar 30, 2024
8042899
fix escape keydown behavior in all builders
anatolzak Mar 30, 2024
bb41131
test(escape-keydown): add tests for sibling elements with different …
anatolzak Mar 30, 2024
9a08d77
add changeset
anatolzak Mar 30, 2024
2877afe
fix(dialog): should not add dialog to escape-keydown stack until is open
anatolzak Mar 30, 2024
9ac37f5
Merge branch 'develop' into fix/escape-keydown-nested-elements
anatolzak Mar 31, 2024
c50f81c
improve type safety in tests
anatolzak Mar 31, 2024
2a31851
fix types in escape keydown tests
anatolzak Mar 31, 2024
368cace
fix menubar tests
anatolzak Mar 31, 2024
453f4a1
feat(escape-keydown): change `closeOnEscape` to `escapeBehavior` & en…
anatolzak Apr 1, 2024
a9cc1dd
add tests for new escape keydown action
anatolzak Apr 1, 2024
d16393d
docs: update escape behavior description
anatolzak Apr 1, 2024
e9f099d
update `closeOnEscape` references to `escapeBehavior`
anatolzak Apr 1, 2024
df92431
changeset
anatolzak Apr 1, 2024
afc292c
remove unnecessary changes
anatolzak Apr 1, 2024
51d8538
fix escape keydown tests
anatolzak Apr 1, 2024
bd6a3bc
Merge branch 'develop' into fix/escape-keydown-nested-elements
anatolzak Apr 1, 2024
1098c14
Merge branch 'develop' into fix/escape-keydown-nested-elements
anatolzak Apr 1, 2024
dba7c36
prettier
anatolzak Apr 1, 2024
3db9691
Merge branch 'fix/escape-keydown-nested-elements' of https://github.c…
anatolzak Apr 1, 2024
f518f38
Merge branch 'develop' into fix/escape-keydown-nested-elements
anatolzak Apr 1, 2024
f54957c
fix tests
anatolzak Apr 1, 2024
17988c8
Merge branch 'develop' into fix/escape-keydown-nested-elements
anatolzak Apr 1, 2024
73eade6
allow changing `escapeBehavior` without reseting position of element …
anatolzak Apr 2, 2024
acb267d
test: allow changing `escapeBehavior` without reseting position of el…
anatolzak Apr 2, 2024
0c1a6cd
Merge branch 'fix/escape-keydown-nested-elements' of https://github.c…
anatolzak Apr 2, 2024
7a73dc8
Merge branch 'develop' into fix/escape-keydown-nested-elements
anatolzak Apr 5, 2024
54777c8
feat: add escape behavior types: `defer-otherwise-close` and `defer-o…
anatolzak Apr 5, 2024
cbe8b33
add tests for new escape behavior types: `defer-otherwise-close` and …
anatolzak Apr 5, 2024
39e373e
docs: add descriptions for new escape behavior types: `defer-otherwis…
anatolzak Apr 5, 2024
9212f2a
change builder `escapeBehavior` JSDoc for new escape behavior types: …
anatolzak Apr 5, 2024
fac04c2
fix: landing page popover `escapeBehavior` prop value
anatolzak Apr 5, 2024
7831713
fix tests for new escapeBehavior values `defer-otherwise-close` and `…
anatolzak Apr 5, 2024
e8c0de5
fix svelte-check
anatolzak Apr 5, 2024
0cc0f25
update escape keydown tests function name
anatolzak Apr 5, 2024
4e727bb
revert dialog content actions execution order
anatolzak Apr 8, 2024
c108bb1
Merge branch 'develop' into fix/escape-keydown-nested-elements
anatolzak Apr 16, 2024
d963b4f
lint
anatolzak Apr 16, 2024
63d3110
lint
anatolzak Apr 16, 2024
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
5 changes: 5 additions & 0 deletions .changeset/rude-snakes-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 1,5 @@
---
'@melt-ui/svelte': minor
---

Fixed escape keydown behavior by using stacks to correctly handle nested floating elements
5 changes: 5 additions & 0 deletions .changeset/witty-games-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 1,5 @@
---
'@melt-ui/svelte': minor
---

Changed `closeOnEscape` builder prop to `escapeBehavior` to provide even further fine-grained control over escape behavior in builders (closes #1142)
27 changes: 20 additions & 7 deletions src/docs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 13,17 @@ export const SEE = {
},
};

const createBulletsHTML = (bullets: string[]) => {
return bullets.map((bullet) => `<li>${bullet}</li>`).join('');
};

const ESCAPE_BEHAVIOR_BULLETS = (name: string) => [
`\`close\`: Closes the ${name} immediately.`,
`\`ignore\`: Prevents the ${name} from closing and also blocks the parent element from closing in response to the Escape key.`,
`\`defer-otherwise-close\`: Delegates the action to the parent element. If no parent is found, it closes the element.`,
`\`defer-otherwise-ignore\`: Delegates the action to the parent element. If no parent is found, nothing is done.`,
];

export const DESCRIPTIONS = {
FLOATING_CONFIG:
'A configuration object which determines how the floating element is positioned relative to the trigger.',
Expand All @@ -22,8 33,10 @@ export const DESCRIPTIONS = {
ON_SELECT:
'A callback which is called when the item is selected. To prevent the default behavior, call `e.preventDefault()` in the callback.',
LOOP: 'Whether or not the focus should loop back to the first item when the last item is reached.',
CLOSE_ON_ESCAPE: (name = 'element') =>
`Whether or not to close the ${name} when the escape key is pressed.`,
ESCAPE_BEHAVIOR: (name = 'element') =>
`Defines how the ${name} reacts when the Escape key is pressed. ${createBulletsHTML(
ESCAPE_BEHAVIOR_BULLETS(name)
)}`,
CLOSE_ON_CLICK_OUTSIDE: (name = 'element') =>
`Whether or not to close the ${name} when the user clicks outside of it.`,
BUILDER: (name: string) => `The builder function used to create the ${name} component.`,
Expand Down Expand Up @@ -83,11 96,11 @@ export const PROPS = {
description: DESCRIPTIONS.PORTAL(args.name ?? 'floating element'),
default: args.default ?? 'body',
}),
CLOSE_ON_ESCAPE: (args: PropArgs = {}): Prop => ({
name: 'closeOnEscape',
type: 'boolean',
description: DESCRIPTIONS.CLOSE_ON_ESCAPE(args.name ?? 'floating element'),
default: args.default ?? 'true',
ESCAPE_BEHAVIOR: (args: PropArgs = {}): Prop => ({
name: 'escapeBehavior',
type: '"close" | "ignore" | "defer-otherwise-close" | "defer-otherwise-ignore"',
description: DESCRIPTIONS.ESCAPE_BEHAVIOR(args.name ?? 'floating element'),
default: args.default ?? "'close'",
}),
CLOSE_ON_OUTSIDE_CLICK: (args: PropArgs = {}): Prop => ({
name: 'closeOnOutsideClick',
Expand Down
4 changes: 2 additions & 2 deletions src/docs/content/builders/dialog.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 99,11 @@ const {
### Disable Close on Escape

By default, pressing the escape key will close the dialog. You can disable this behavior by setting
the `closeOnEscape` builder prop to `false`.
the `escapeBehavior` builder prop to `ignore`.

```ts {2}
const dialog = createDialog({
closeOnEscape: false
escapeBehavior: 'ignore'
})
```

Expand Down
2 changes: 1 addition & 1 deletion src/docs/data/builders/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 18,7 @@ const OPTION_PROPS = [
},
PROPS.LOOP,
PROPS.CLOSE_ON_OUTSIDE_CLICK,
PROPS.CLOSE_ON_ESCAPE,
PROPS.ESCAPE_BEHAVIOR,
PROPS.PREVENT_SCROLL,
PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
PROPS.PORTAL,
Expand Down
2 changes: 1 addition & 1 deletion src/docs/data/builders/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 16,7 @@ const OPTION_PROPS = [
description: 'The `role` attribute of the dialog element.',
},
PROPS.PREVENT_SCROLL,
PROPS.CLOSE_ON_ESCAPE,
PROPS.ESCAPE_BEHAVIOR,
PROPS.CLOSE_ON_OUTSIDE_CLICK,
PROPS.PORTAL,
PROPS.FORCE_VISIBLE,
Expand Down
2 changes: 1 addition & 1 deletion src/docs/data/builders/link-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 24,7 @@ const OPTION_PROPS = [
PROPS.POSITIONING({ default: "placement: 'bottom'" }),
PROPS.ARROW_SIZE,
PROPS.CLOSE_ON_OUTSIDE_CLICK,
PROPS.CLOSE_ON_ESCAPE,
PROPS.ESCAPE_BEHAVIOR,
PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
PROPS.FORCE_VISIBLE,
PROPS.PORTAL,
Expand Down
4 changes: 2 additions & 2 deletions src/docs/data/builders/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 17,7 @@ export const menuBuilderProps = [
PROPS.ARROW_SIZE,
PROPS.DIR,
PROPS.PREVENT_SCROLL,
PROPS.CLOSE_ON_ESCAPE,
PROPS.ESCAPE_BEHAVIOR,
PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
PROPS.PORTAL,
PROPS.CLOSE_ON_OUTSIDE_CLICK,
Expand All @@ -39,7 39,7 @@ export const menuBuilderOptions = [
PROPS.ARROW_SIZE,
PROPS.DIR,
PROPS.PREVENT_SCROLL,
PROPS.CLOSE_ON_ESCAPE,
PROPS.ESCAPE_BEHAVIOR,
PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
PROPS.PORTAL,
PROPS.CLOSE_ON_OUTSIDE_CLICK,
Expand Down
2 changes: 1 addition & 1 deletion src/docs/data/builders/menubar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 7,7 @@ import { builder as dropdownBuilder } from './dropdown-menu.js';
import type { BuilderData } from './index.js';
import { getMenuSchemas, getMenuTriggerDataAttrs } from './menu.js';

const OPTION_PROPS = [PROPS.CLOSE_ON_ESCAPE, PROPS.LOOP, PROPS.PREVENT_SCROLL];
const OPTION_PROPS = [PROPS.ESCAPE_BEHAVIOR, PROPS.LOOP, PROPS.PREVENT_SCROLL];
const BUILDER_NAME = 'menubar';

const builder = builderSchema(BUILDER_NAME, {
Expand Down
2 changes: 1 addition & 1 deletion src/docs/data/builders/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 22,7 @@ const OPTION_PROPS = [
description: 'Whether to disable the focus trap.',
},
PROPS.ARROW_SIZE,
PROPS.CLOSE_ON_ESCAPE,
PROPS.ESCAPE_BEHAVIOR,
PROPS.CLOSE_ON_OUTSIDE_CLICK,
PROPS.PREVENT_SCROLL,
PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
Expand Down
2 changes: 1 addition & 1 deletion src/docs/data/builders/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 19,7 @@ const OPTION_PROPS = [
PROPS.ARROW_SIZE,
PROPS.PREVENT_SCROLL,
PROPS.LOOP,
PROPS.CLOSE_ON_ESCAPE,
PROPS.ESCAPE_BEHAVIOR,
PROPS.CLOSE_ON_OUTSIDE_CLICK,
PROPS.PREVENT_TEXT_SELECTION_OVERFLOW,
PROPS.PORTAL,
Expand Down
2 changes: 1 addition & 1 deletion src/docs/data/builders/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 16,7 @@ import type { BuilderData } from './index.js';
const OPTION_PROPS = [
PROPS.POSITIONING({ default: "position: 'top'" }),
PROPS.ARROW_SIZE,
PROPS.CLOSE_ON_ESCAPE,
PROPS.ESCAPE_BEHAVIOR,
PROPS.FORCE_VISIBLE,
PROPS.PORTAL,
{
Expand Down
14 changes: 0 additions & 14 deletions src/lib/builders/combobox/create.ts
Original file line number Diff line number Diff line change
@@ -1,4 1,3 @@
import { useEscapeKeydown } from '$lib/internal/actions/index.js';
import {
addEventListener,
addMeltEventListener,
Expand All @@ -9,7 8,6 @@ import {
isContentEditable,
isHTMLInputElement,
kbd,
noop,
omit,
} from '$lib/internal/helpers/index.js';
import type { MeltActionReturn } from '$lib/internal/types.js';
Expand Down Expand Up @@ -71,24 69,12 @@ export function createCombobox<
})
);

let unsubEscapeKeydown = noop;

const escape = useEscapeKeydown(node, {
handler: () => {
listbox.helpers.closeMenu();
},
});
if (escape && escape.destroy) {
unsubEscapeKeydown = escape.destroy;
}

const { destroy } = listbox.elements.trigger(node);

return {
destroy() {
destroy?.();
unsubscribe();
unsubEscapeKeydown();
},
};
},
Expand Down
17 changes: 5 additions & 12 deletions src/lib/builders/context-menu/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 46,7 @@ const defaults = {
placement: 'bottom-start',
},
preventScroll: true,
closeOnEscape: true,
escapeBehavior: 'close',
closeOnOutsideClick: true,
portal: undefined,
loop: false,
Expand All @@ -72,7 72,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
closeOnOutsideClick,
portal,
forceVisible,
closeOnEscape,
escapeBehavior,
loop,
preventTextSelectionOverflow,
} = rootOptions;
Expand Down Expand Up @@ -149,15 149,8 @@ export function createContextMenu(props?: CreateContextMenuProps) {
let unsubPopper = noop;

const unsubDerived = effect(
[isVisible, rootActiveTrigger, positioning, closeOnOutsideClick, portal, closeOnEscape],
([
$isVisible,
$rootActiveTrigger,
$positioning,
$closeOnOutsideClick,
$portal,
$closeOnEscape,
]) => {
[isVisible, rootActiveTrigger, positioning, closeOnOutsideClick, portal],
([$isVisible, $rootActiveTrigger, $positioning, $closeOnOutsideClick, $portal]) => {
unsubPopper();
if (!$isVisible || !$rootActiveTrigger) return;
tick().then(() => {
Expand All @@ -177,7 170,7 @@ export function createContextMenu(props?: CreateContextMenuProps) {
shouldCloseOnInteractOutside: handleClickOutside,
},
portal: getPortalDestination(node, $portal),
escapeKeydown: $closeOnEscape ? undefined : null,
escapeKeydown: { behaviorType: escapeBehavior },
preventTextSelectionOverflow: { enabled: preventTextSelectionOverflow },
},
}).destroy;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/builders/date-picker/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 25,7 @@ const defaults = {
positioning: {
placement: 'bottom',
},
closeOnEscape: true,
escapeBehavior: 'close',
closeOnOutsideClick: true,
onOutsideClick: undefined,
preventScroll: false,
Expand Down Expand Up @@ -77,7 77,7 @@ export function createDatePicker(props?: CreateDatePickerProps) {
defaultOpen: withDefaults.defaultOpen,
open: withDefaults.open,
disableFocusTrap: withDefaults.disableFocusTrap,
closeOnEscape: withDefaults.closeOnEscape,
escapeBehavior: withDefaults.escapeBehavior,
preventScroll: withDefaults.preventScroll,
onOpenChange: withDefaults.onOpenChange,
closeOnOutsideClick: withDefaults.closeOnOutsideClick,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/builders/date-range-picker/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 25,7 @@ const defaults = {
positioning: {
placement: 'bottom',
},
closeOnEscape: true,
escapeBehavior: 'close',
closeOnOutsideClick: true,
preventScroll: false,
forceVisible: false,
Expand Down Expand Up @@ -61,7 61,7 @@ export function createDateRangePicker(props?: CreateDateRangePickerProps) {
defaultOpen: withDefaults.defaultOpen,
open: withDefaults.open,
disableFocusTrap: withDefaults.disableFocusTrap,
closeOnEscape: withDefaults.closeOnEscape,
escapeBehavior: withDefaults.escapeBehavior,
preventScroll: withDefaults.preventScroll,
onOpenChange: withDefaults.onOpenChange,
closeOnOutsideClick: withDefaults.closeOnOutsideClick,
Expand Down
32 changes: 6 additions & 26 deletions src/lib/builders/dialog/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 39,7 @@ const { name } = createElHelpers<DialogParts>('dialog');

const defaults = {
preventScroll: true,
closeOnEscape: true,
escapeBehavior: 'close',
closeOnOutsideClick: true,
role: 'dialog',
defaultOpen: false,
Expand All @@ -60,7 60,7 @@ export function createDialog(props?: CreateDialogProps) {

const {
preventScroll,
closeOnEscape,
escapeBehavior,
closeOnOutsideClick,
role,
portal,
Expand Down Expand Up @@ -135,26 135,6 @@ export function createDialog(props?: CreateDialogProps) {
'data-state': $open ? 'open' : 'closed',
} as const;
},
action: (node: HTMLElement) => {
let unsubEscapeKeydown = noop;

if (closeOnEscape.get()) {
const escapeKeydown = useEscapeKeydown(node, {
handler: () => {
handleClose();
},
});
if (escapeKeydown && escapeKeydown.destroy) {
unsubEscapeKeydown = escapeKeydown.destroy;
}
}

return {
destroy() {
unsubEscapeKeydown();
},
};
},
});

const content = makeElement(name('content'), {
Expand All @@ -174,13 154,13 @@ export function createDialog(props?: CreateDialogProps) {
},

action: (node: HTMLElement) => {
let unsubModal = noop;
let unsubEscape = noop;
let unsubModal = noop;
let unsubFocusTrap = noop;

const unsubDerived = effect(
[isVisible, closeOnOutsideClick, closeOnEscape],
([$isVisible, $closeOnOutsideClick, $closeOnEscape]) => {
[isVisible, closeOnOutsideClick],
([$isVisible, $closeOnOutsideClick]) => {
unsubModal();
unsubEscape();
unsubFocusTrap();
Expand All @@ -198,7 178,7 @@ export function createDialog(props?: CreateDialogProps) {

unsubEscape = useEscapeKeydown(node, {
handler: handleClose,
enabled: $closeOnEscape,
behaviorType: escapeBehavior,
}).destroy;

unsubFocusTrap = useFocusTrap(node, { fallbackFocus: node }).destroy;
Expand Down
11 changes: 8 additions & 3 deletions src/lib/builders/dialog/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 2,7 @@ import type { ChangeFn, FocusProp, IdObj } from '$lib/internal/helpers/index.js'
import type { BuilderReturn } from '$lib/internal/types.js';
import type { Writable } from 'svelte/store';
import type { DialogIdParts, createDialog } from './create.js';
import type { EscapeBehaviorType } from '$lib/internal/actions/index.js';
export type { DialogComponentEvents } from './events.js';
export type CreateDialogProps = {
/**
Expand All @@ -13,11 14,15 @@ export type CreateDialogProps = {
preventScroll?: boolean;

/**
* If true, the dialog will close when the user presses the escape key.
* Escape behavior type.
* `close`: Closes the element immediately.
* `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element.
* `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done.
* `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to the Escape key.
*
* @default true
* @defaultValue `close`
*/
closeOnEscape?: boolean;
escapeBehavior?: EscapeBehaviorType;

/**
* If true, the dialog will close when the user clicks outside of it.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/builders/dropdown-menu/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 11,7 @@ const defaults = {
placement: 'bottom',
},
preventScroll: true,
closeOnEscape: true,
escapeBehavior: 'close',
closeOnOutsideClick: true,
portal: undefined,
loop: false,
Expand Down
Loading
Loading