Skip to content

Commit

Permalink
Merge branch 'main' into store-controlled-props-2
Browse files Browse the repository at this point in the history
  • Loading branch information
diegohaz committed Mar 25, 2024
2 parents 36dc444 + 327f704 commit 3eee38f
Show file tree
Hide file tree
Showing 16 changed files with 697 additions and 607 deletions.
5 changes: 5 additions & 0 deletions .changeset/3608-dom-utils-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ariakit/core": patch
---

Added DOM utils: `isTextbox`, `getTextboxValue`.
5 changes: 5 additions & 0 deletions .changeset/3608-dom-utils-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ariakit/core": patch
---

Added event function: `getInputType`.
5 changes: 5 additions & 0 deletions .changeset/3609-ariakit-test-within.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ariakit/test": patch
---

Added `within` function to queries.
6 changes: 6 additions & 0 deletions .changeset/3610-combobox-remove-flush-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ariakit/react-core": patch
"@ariakit/react": patch
---

Updated [`Combobox`](https://ariakit.org/reference/combobox) to no longer use `ReactDOM.flushSync` when updating the value.
7 changes: 7 additions & 0 deletions .changeset/improve-jsdocs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@ariakit/core": patch
"@ariakit/react": patch
"@ariakit/react-core": patch
---

Improved JSDocs.
1,004 changes: 519 additions & 485 deletions package-lock.json

Large diffs are not rendered by default.

40 changes: 20 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@
}
},
"dependencies": {
"@babel/core": "7.24.1",
"@babel/preset-env": "7.24.1",
"@babel/core": "7.24.3",
"@babel/preset-env": "7.24.3",
"@babel/preset-react": "7.24.1",
"@babel/preset-typescript": "7.24.1",
"@babel/types": "7.24.0",
Expand All @@ -88,9 +88,9 @@
"@radix-ui/react-popover": "1.0.7",
"@radix-ui/react-select": "2.0.0",
"@stackblitz/sdk": "1.9.0",
"@stripe/react-stripe-js": "2.5.1",
"@stripe/react-stripe-js": "2.6.2",
"@stripe/stripe-js": "2.4.0",
"@tanstack/react-query": "5.24.1",
"@tanstack/react-query": "5.28.6",
"@testing-library/dom": "9.3.4",
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "14.2.2",
Expand All @@ -102,8 +102,8 @@
"@types/marked": "5.0.2",
"@types/node": "18.19.26",
"@types/postcss-import": "14.0.3",
"@types/prettier": "2.7.3",
"@types/react": "18.2.67",
"@types/prettier": "3.0.0",
"@types/react": "18.2.69",
"@types/react-dom": "18.2.22",
"@types/stylis": "4.2.5",
"@types/textarea-caret": "3.0.3",
Expand All @@ -112,16 +112,16 @@
"@typescript-eslint/parser": "7.3.1",
"@vercel/analytics": "1.2.2",
"@vitest/coverage-c8": "0.33.0",
"@wordpress/components": "27.1.0",
"autoprefixer": "10.4.18",
"@wordpress/components": "27.2.0",
"autoprefixer": "10.4.19",
"babylon": "6.18.0",
"chalk": "5.3.0",
"chokidar": "3.6.0",
"clsx": "2.1.0",
"concurrently": "8.2.2",
"cross-env": "7.0.3",
"cross-spawn": "7.0.3",
"date-fns": "3.4.0",
"date-fns": "3.6.0",
"dotenv": "16.4.5",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
Expand Down Expand Up @@ -149,15 +149,15 @@
"monaco-editor-webpack-plugin": "7.1.0",
"monaco-textmate": "3.0.1",
"monaco-vscode-textmate-theme-converter": "0.1.7",
"next": "14.1.3",
"next": "14.1.4",
"null-loader": "4.0.1",
"onigasm": "2.2.5",
"open-cli": "8.0.0",
"parse-numeric-range": "1.3.0",
"patch-package": "8.0.0",
"postcss": "8.4.35",
"postcss": "8.4.38",
"postcss-combine-duplicated-selectors": "10.0.3",
"postcss-import": "16.0.1",
"postcss-import": "16.1.0",
"postcss-merge-selectors": "0.0.6",
"postcss-prettify": "0.3.4",
"prettier": "3.2.5",
Expand All @@ -170,35 +170,35 @@
"react-shadow": "20.4.0",
"react-toastify": "10.0.5",
"read-pkg-up": "11.0.0",
"recast": "0.23.5",
"recast": "0.23.6",
"rehype": "13.0.1",
"rehype-parse": "8.0.5",
"rehype-raw": "6.1.1",
"rehype-slug": "5.1.0",
"remark-gfm": "3.0.1",
"resolve-from": "5.0.0",
"rimraf": "5.0.5",
"sharp": "0.33.2",
"shiki": "1.1.7",
"stripe": "14.20.0",
"stylelint": "16.2.1",
"sharp": "0.33.3",
"shiki": "1.2.0",
"stripe": "14.22.0",
"stylelint": "16.3.0",
"stylelint-config-standard": "36.0.0",
"stylis": "4.3.1",
"sugarss": "4.0.1",
"tailwind-merge": "2.2.1",
"tailwind-merge": "2.2.2",
"tailwindcss": "3.4.1",
"tailwindcss-animate": "1.0.7",
"textarea-caret": "3.1.0",
"tiny-invariant": "1.3.3",
"ts-morph": "22.0.0",
"tsup": "8.0.2",
"typescript": "5.4.2",
"typescript": "5.4.3",
"unified": "10.1.2",
"unist-util-visit": "5.0.0",
"use-local-storage-state": "19.1.0",
"vitest": "1.4.0",
"vitest-fail-on-console": "0.6.3",
"webpack": "5.90.3",
"webpack": "5.91.0",
"zod": "3.22.4"
}
}
75 changes: 49 additions & 26 deletions packages/ariakit-core/src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,36 +190,26 @@ export function isTextField(
}

/**
* Returns the element's role attribute, if it has one.
* Check whether the given element is a text field or a content editable
* element.
*/
export function getPopupRole(
element?: Element | null,
fallback?: AriaHasPopup,
) {
const allowedPopupRoles = ["dialog", "menu", "listbox", "tree", "grid"];
const role = element?.getAttribute("role");
if (role && allowedPopupRoles.indexOf(role) !== -1) {
return role as "dialog" | "menu" | "listbox" | "tree" | "grid";
}
return fallback;
export function isTextbox(element: HTMLElement) {
return element.isContentEditable || isTextField(element);
}

/**
* Returns the item role attribute based on the popup's role.
* Returns the value of the text field or content editable element as a string.
*/
export function getPopupItemRole(
element?: Element | null,
fallback?: AriaRole,
) {
const itemRoleByPopupRole = {
menu: "menuitem",
listbox: "option",
tree: "treeitem",
};
const popupRole = getPopupRole(element);
if (!popupRole) return fallback;
const key = popupRole as keyof typeof itemRoleByPopupRole;
return itemRoleByPopupRole[key] ?? fallback;
export function getTextboxValue(element: HTMLElement) {
if (isTextField(element)) {
return element.value;
}
if (element.isContentEditable) {
const range = getDocument(element).createRange();
range.selectNodeContents(element);
return range.toString();
}
return "";
}

/**
Expand Down Expand Up @@ -252,6 +242,39 @@ export function getTextboxSelection(element: HTMLElement) {
return { start, end };
}

/**
* Returns the element's role attribute, if it has one.
*/
export function getPopupRole(
element?: Element | null,
fallback?: AriaHasPopup,
) {
const allowedPopupRoles = ["dialog", "menu", "listbox", "tree", "grid"];
const role = element?.getAttribute("role");
if (role && allowedPopupRoles.indexOf(role) !== -1) {
return role as "dialog" | "menu" | "listbox" | "tree" | "grid";
}
return fallback;
}

/**
* Returns the item role attribute based on the popup's role.
*/
export function getPopupItemRole(
element?: Element | null,
fallback?: AriaRole,
) {
const itemRoleByPopupRole = {
menu: "menuitem",
listbox: "option",
tree: "treeitem",
};
const popupRole = getPopupRole(element);
if (!popupRole) return fallback;
const key = popupRole as keyof typeof itemRoleByPopupRole;
return itemRoleByPopupRole[key] ?? fallback;
}

/**
* Calls `element.scrollIntoView()` if the element is hidden or partly hidden in
* the viewport.
Expand Down Expand Up @@ -325,7 +348,7 @@ export function isPartiallyHidden(element: Element) {
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange
*/
export function setSelectionRange(
element: HTMLInputElement,
element: HTMLInputElement | HTMLTextAreaElement,
...args: Parameters<typeof HTMLInputElement.prototype.setSelectionRange>
) {
if (/text|search|password|tel|url/i.test(element.type)) {
Expand Down
11 changes: 11 additions & 0 deletions packages/ariakit-core/src/utils/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,17 @@ export function isFocusEventOutside(
return !relatedTarget || !contains(containerElement, relatedTarget);
}

/**
* Returns the `inputType` property of the event, if available.
*/
export function getInputType(event: Event | { nativeEvent: Event }) {
const nativeEvent = "nativeEvent" in event ? event.nativeEvent : event;
if (!nativeEvent) return;
if (!("inputType" in nativeEvent)) return;
if (typeof nativeEvent.inputType !== "string") return;
return nativeEvent.inputType;
}

/**
* Runs a callback on the next animation frame, but before a certain event.
*/
Expand Down
19 changes: 7 additions & 12 deletions packages/ariakit-react-core/src/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
MouseEvent,
FocusEvent as ReactFocusEvent,
KeyboardEvent as ReactKeyboardEvent,
SyntheticEvent,
} from "react";
import {
getPopupRole,
Expand All @@ -27,7 +28,6 @@ import type {
BooleanOrCallback,
StringWithValue,
} from "@ariakit/core/utils/types";
import { flushSync } from "react-dom";
import type { CompositeOptions } from "../composite/composite.js";
import { useComposite } from "../composite/composite.js";
import type { PopoverAnchorOptions } from "../popover/popover-anchor.js";
Expand Down Expand Up @@ -366,7 +366,10 @@ export const useCombobox = createHook<TagName, ComboboxOptions>(
};
}, [inline, contentElement, store, value]);

const canShow = value.length >= showMinLength;
const canShow = (event: SyntheticEvent) => {
const currentTarget = event.currentTarget as HTMLType;
return currentTarget.value.length >= showMinLength;
};

const onChangeProp = props.onChange;
const showOnChangeProp = useBooleanEvent(showOnChange ?? canShow);
Expand All @@ -376,7 +379,7 @@ export const useCombobox = createHook<TagName, ComboboxOptions>(
onChangeProp?.(event);
if (event.defaultPrevented) return;
if (!store) return;
const { value, selectionStart, selectionEnd } = event.target;
const { value, selectionStart } = event.currentTarget;
const nativeEvent = event.nativeEvent;
canAutoSelectRef.current = true;
if (isInputEvent(nativeEvent)) {
Expand All @@ -394,15 +397,7 @@ export const useCombobox = createHook<TagName, ComboboxOptions>(
}
if (setValueOnChangeProp(event)) {
const isSameValue = value === store.getState().value;
flushSync(() => store?.setValue(value));
// When the value is not set synchronously, the selection range may be
// lost. Even setting the value with flushSync above, we still need to
// fix the selection range because React's useSyncExternalStore updates
// in a microtask. An alternative fix would be removing the flushSync
// call above and calling setSelectionRange in a queueMicrotask
// callback, but I think flushSync is a safer approach. See
// combobox-group "keep caret position when typing" test.
setSelectionRange(event.currentTarget, selectionStart, selectionEnd);
store.setValue(value);
if (inline && autoSelect && isSameValue) {
// The store.setValue(event.target.value) above may not trigger a
// state update. For example, say the first item starts with "t". The
Expand Down
32 changes: 6 additions & 26 deletions packages/ariakit-react-core/src/composite/composite-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import type {
} from "react";
import { useCallback, useContext, useMemo, useRef, useState } from "react";
import {
getDocument,
getScrollingElement,
getTextboxSelection,
getTextboxValue,
isButton,
isTextField,
isTextbox,
} from "@ariakit/core/utils/dom";
import { isPortalEvent, isSelfTarget } from "@ariakit/core/utils/events";
import {
Expand Down Expand Up @@ -56,27 +57,11 @@ const TagName = "button" satisfies ElementType;
type TagName = typeof TagName;
type HTMLType = HTMLElementTagNameMap[TagName];

function isTextbox(element: HTMLElement) {
return element.isContentEditable || isTextField(element);
}

function isEditableElement(element: HTMLElement) {
if (element.isContentEditable) return true;
if (isTextField(element)) return true;
if (isTextbox(element)) return true;
return element.tagName === "INPUT" && !isButton(element);
}

function getValueLength(element: HTMLElement) {
if (isTextField(element)) {
return element.value.length;
} else if (element.isContentEditable) {
const range = getDocument(element).createRange();
range.selectNodeContents(element);
return range.toString().length;
}
return 0;
}

function getNextPageOffset(scrollingElement: Element, pageUp = false) {
const height = scrollingElement.clientHeight;
const { top } = scrollingElement.getBoundingClientRect();
Expand Down Expand Up @@ -370,14 +355,9 @@ export const useCompositeItem = createHook<TagName, CompositeItemOptions>(
const isUp = isVertical && event.key === "ArrowUp";
const isDown = isVertical && event.key === "ArrowDown";
if (isRight || isDown) {
if (selection.end !== getValueLength(currentTarget)) {
return;
}
} else if (isLeft || isUp) {
if (selection.start !== 0) {
return;
}
}
const { length: valueLength } = getTextboxValue(currentTarget);
if (selection.end !== valueLength) return;
} else if ((isLeft || isUp) && selection.start !== 0) return;
}
const nextId = action();
if (preventScrollOnKeyDownProp(event) || nextId !== undefined) {
Expand Down
Loading

0 comments on commit 3eee38f

Please sign in to comment.