Skip to content

feat(focus): add createFocusGroup - #1004

Merged
davedbase merged 2 commits into
solidjs-community:nextfrom
jer3m01:feat/focus
Aug 9, 2026
Merged

feat(focus): add createFocusGroup#1004
davedbase merged 2 commits into
solidjs-community:nextfrom
jer3m01:feat/focus

Conversation

@jer3m01

@jer3m01 jer3m01 commented Aug 9, 2026

Copy link
Copy Markdown
Member

Ports FocusManager from Kobalte to focus.

Also handles keyboard navigation within the focus group.

import { createFocusGroup } from "@solid-primitives/focus";

const [ref, setRef] = createSignal<HTMLElement>();

// Keyboard navigation is attached to the ref automatically.
createFocusGroup(ref);

return (
  <div ref={setRef} role="menu">
    <button role="menuitem">One</button>
    <button role="menuitem">Two</button>
    <button role="menuitem">Three</button>
  </div>
);

Summary by CodeRabbit

  • New Features
    • Added focus group management for directional and sequential keyboard navigation.
    • Supports Arrow keys, Home/End, optional Tab handling, wrapping, orientation, RTL layouts, and configurable focus filtering.
    • Added programmatic methods to focus the first, last, next, or previous element.
  • Documentation
    • Added usage guidance and configuration details for focus groups.
  • Tests
    • Added comprehensive browser and server-side coverage for focus navigation and configuration.

@changeset-bot

changeset-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3238a1a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solid-primitives/focus Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds createFocusGroup with programmatic and keyboard focus navigation, configurable focus filtering, visibility and radio handling, public exports, documentation, a Storybook example, release metadata, and SSR coverage.

Changes

Focus group primitive

Layer / File(s) Summary
Focus contracts and visibility filters
packages/focus/src/focusGroup.ts, packages/focus/src/tabbable.ts
Defines focus-group options and APIs. Adds focusable and tabbable selectors with recursive visibility checks.
Programmatic focus traversal
packages/focus/src/focusGroup.ts, packages/focus/test/index.test.tsx
Implements first, last, next, and previous focus methods with wrapping, filtering, acceptance predicates, TreeWalker traversal, and radio-group handling.
Keyboard navigation and package integration
packages/focus/src/focusGroup.ts, packages/focus/src/index.ts, packages/focus/stories/focus.stories.tsx, packages/focus/test/index.test.tsx
Adds reactive Arrow, Home, End, and optional Tab navigation. Exports the new API and adds an interactive story with keyboard-navigation tests.
Documentation, release metadata, and SSR validation
packages/focus/README.md, packages/focus/package.json, .changeset/public-crabs-joke.md, packages/focus/test/server.test.ts
Documents createFocusGroup, updates the primitive list, adds patch-release metadata, and tests SSR initialization and disposal.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested labels: enhancement

Suggested reviewers: davedbase

Sequence Diagram(s)

sequenceDiagram
  participant Consumer
  participant createFocusGroup
  participant RootElement
  participant getFocusableTreeWalker
  participant FocusTarget
  Consumer->>createFocusGroup: create group with root and options
  createFocusGroup->>RootElement: attach keydown listener
  RootElement->>createFocusGroup: receive navigation key
  createFocusGroup->>getFocusableTreeWalker: resolve focus candidates
  getFocusableTreeWalker-->>createFocusGroup: return filtered candidates
  createFocusGroup->>FocusTarget: focus selected element
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding createFocusGroup to the focus package.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jer3m01 jer3m01 changed the title feat(focus): add focusGroup feat(focus): add createFocusGroup Aug 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (10)
packages/focus/src/focusGroup.ts (4)

108-112: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

from is never passed to the walker, so the subtree-reject branch is dead for these callers.

getFocusableTreeWalker contains a filter branch that rejects nodes inside opts.from (lines 320-323), and it also sets walker.currentNode = opts.from (lines 358-360). focusNext and focusPrevious omit from from the options object and assign currentNode themselves. As a result, if from is a container that itself holds focusable descendants, focusNext moves focus into that subtree instead of past it. The upstream Kobalte implementation passes from to the walker to avoid this.

Either pass from through, or drop the reject branch and the currentNode assignment inside getFocusableTreeWalker so the helper has one clear contract.

♻️ Option: pass `from` through
-    const walker = getFocusableTreeWalker(root, { tabbable, accept });
-
-    if (from && root.contains(from)) {
-      walker.currentNode = from;
-    }
+    const walker = getFocusableTreeWalker(
+      root,
+      from && root.contains(from) ? { tabbable, accept, from } : { tabbable, accept },
+    );

Also applies to: 142-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/src/focusGroup.ts` around lines 108 - 112, Update the
focusNext and focusPrevious walker construction to pass the from value in the
options supplied to getFocusableTreeWalker, and remove their manual
walker.currentNode assignments so the helper applies its existing from-based
positioning and subtree rejection consistently.

312-316: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document or remove the undocumented scope parameter.

getFocusableTreeWalker is exported from packages/focus/src/index.ts, so scope is public API. No caller in this PR passes it, no test covers it, and the JSDoc does not mention it. Add a @param scope description, or drop the parameter and isElementInScope until a consumer needs them.

Also applies to: 348-348

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/src/focusGroup.ts` around lines 312 - 316, Update the exported
getFocusableTreeWalker API by either documenting the scope parameter in its
JSDoc with its intended behavior, or removing scope and the associated
isElementInScope logic until it has a consumer. Keep the implementation and
public signature consistent with the chosen option.

242-253: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider skipping arrow handling for text-entry targets.

The listener sits on the group root and keydown bubbles. So an <input type="text"> or <textarea> inside the group loses caret movement: in horizontal orientation ArrowLeft and ArrowRight move focus instead of the caret, and Home and End move focus instead of jumping within the value. Menus and toolbars often contain a filter input.

A target check keeps the group usable with text fields.

const isTextEntry = (el: Element | undefined): boolean =>
  el instanceof HTMLTextAreaElement ||
  (el instanceof HTMLInputElement && !/^(button|checkbox|radio|submit|reset)$/.test(el.type)) ||
  (el instanceof HTMLElement && el.isContentEditable);

This is a behavior choice, so treat it as optional. An alternative is to document the limitation in the README.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/src/focusGroup.ts` around lines 242 - 253, Optionally update
the keydown handling around focusNext, focusPrevious, focusFirst, and focusLast
to detect text-entry targets such as text inputs, textareas, and
content-editable elements, then bypass arrow, Home, and End focus handling for
those targets while preserving existing behavior elsewhere.

38-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split per-call options from group-level options.

FocusGroupOptions carries both per-call keys (from, tabbable, wrap, accept) and group-level reactive keys (orientation, textDirection, handleTab, keyboardNavigation). The FocusGroup methods accept the same type, so a caller can pass keyboardNavigation or orientation to focusNext, where the implementation ignores them. Two types make the contract explicit and prevent silent no-ops.

Also, the accept doc states the callback "determines whether the given element is focused". The callback determines whether an element is a focus candidate.

♻️ Proposed type split
-export interface FocusGroupOptions {
+export interface FocusOptions {
   /** The element to start searching from. The currently focused element by default. */
   from?: Element;
 
   /** Whether to only include tabbable elements, or all focusable elements. */
   tabbable?: boolean;
 
   /** Whether focus should wrap around when it reaches the end of the scope. */
   wrap?: boolean;
 
-  /** A callback that determines whether the given element is focused. */
+  /** A callback that determines whether the given element is a focus candidate. */
   accept?: (node: Element) => boolean;
+}
 
+export interface FocusGroupOptions extends FocusOptions {
   /** The orientation of the focus group. `@default` "vertical" */
   orientation?: MaybeAccessor<Orientation>;
@@
   keyboardNavigation?: MaybeAccessor<boolean>;
 }

Then type the FocusGroup methods with FocusOptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/src/focusGroup.ts` around lines 38 - 65, Split
FocusGroupOptions into group-level configuration and a new FocusOptions type
containing from, tabbable, wrap, and accept; update FocusGroup methods to accept
FocusOptions so orientation, textDirection, handleTab, and keyboardNavigation
cannot be passed per call. Correct the accept documentation to describe whether
an element is a focus candidate.
packages/focus/test/index.test.tsx (3)

741-746: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

press never uses its container parameter.

The body calls only flush() and target.dispatchEvent(event). Every call site passes a container that the helper discards. Remove the parameter so the helper signature matches its behavior.

♻️ Proposed fix
   /** Flush pending effects so `createFocusGroup` has attached its keydown listener, then dispatch `event` on `target`. */
-  const press = (container: HTMLElement, target: Element, event: KeyboardEvent) => {
+  const press = (target: Element, event: KeyboardEvent) => {
     flush();
     target.dispatchEvent(event);
     return event;
   };

Then update the call sites to press(buttons[0]!, key("ArrowDown")).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/test/index.test.tsx` around lines 741 - 746, Remove the unused
container parameter from the press helper and update every call site to pass
only the target element and keyboard event, preserving the existing flush and
dispatch behavior.

633-636: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap these createFocusGroup calls in createRoot.

createFocusGroup calls createEffect internally. Every test in this describe block calls it outside a reactive root. Solid logs a warning for computations created outside createRoot, and the effect is never disposed, so each test leaks a keydown listener onto its container. The createFocusGroup keyboard navigation block at line 737 already uses createRoot correctly.

A shared helper keeps the change small.

♻️ Proposed helper
const withRoot = <T,>(fn: () => T): T => {
  let result!: T;
  createRoot(dispose => {
    result = fn();
    onTestFinished(dispose);
  });
  return result;
};

Then call withRoot(() => createFocusGroup(() => container)) in each test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/test/index.test.tsx` around lines 633 - 636, Wrap every
createFocusGroup invocation in the createFocusGroup describe block with a shared
withRoot helper that creates and disposes a Solid root via onTestFinished.
Update the affected tests to call withRoot(() => createFocusGroup(() =>
container)), preserving the existing createFocusGroup behavior and the
already-correct keyboard-navigation test.

703-734: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for radio-group filtering and visibility filtering.

The tests cover tabbable and accept, but two filter paths in getFocusableTreeWalker have no coverage:

  • The radio branch at packages/focus/src/focusGroup.ts lines 325-343, together with getRadiosInGroup and isTabbableRadio at lines 365-395. This is the most intricate logic in the PR. It has form and non-form paths, a RadioNodeList branch, a single-element branch, and a checked/unchecked rule.
  • The isElementVisible filter. No test asserts that a hidden element or a display: none element is skipped.

The handleTab: false option and the focusPrevious branch for a from outside the root (lines 146-152) are also untested.

Do you want me to generate these test cases?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/test/index.test.tsx` around lines 703 - 734, Expand the
focus-group tests to cover radio filtering in getFocusableTreeWalker, including
form and non-form groups, RadioNodeList and single-element paths, and
checked/unchecked behavior via getRadiosInGroup and isTabbableRadio. Add
visibility cases proving hidden and display:none elements are skipped through
isElementVisible, and cover handleTab:false plus focusPrevious when from is
outside the root.
packages/focus/src/tabbable.ts (2)

35-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a comment explaining the join suffix pattern.

Array.prototype.join inserts the separator between entries only. So the suffix :not([hidden]) is not applied to the last entry of focusableElements, and the suffix :not([hidden]):not([tabindex="-1"]) is not applied to the last entry of tabbableElements. Both selectors are correct today because the trailing literal in FOCUSABLE_ELEMENT_SELECTOR and the last entry of tabbableElements carry their own guards. A future entry appended to either array would silently lose its guards.

A short comment above each constant records the constraint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/src/tabbable.ts` around lines 35 - 41, Add concise comments
above FOCUSABLE_ELEMENT_SELECTOR and TABBABLE_ELEMENT_SELECTOR documenting that
join separators apply guards only between entries, so the final array entry must
include its own hidden/tabindex exclusions and future entries must preserve
those guards.

49-56: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

isElementVisible walks to the document root for every candidate.

The function recurses through every ancestor and calls getComputedStyle at each level. getFocusableTreeWalker calls it once per candidate node. For a group with many items inside a deeply nested tree, this produces candidates × depth forced style resolutions per navigation keypress.

This matches the upstream react-spectrum implementation, so it is acceptable for the initial release. If a consumer reports slow arrow navigation in large lists, an ancestor-result cache scoped to one traversal removes the repeated work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/src/tabbable.ts` around lines 49 - 56, Keep isElementVisible
unchanged for this initial release; the comment documents a potential
performance optimization rather than a required fix. If optimization becomes
necessary, add an ancestor-result cache scoped to a single
getFocusableTreeWalker traversal and reuse it across candidate visibility
checks.
packages/focus/stories/focus.stories.tsx (1)

336-337: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Complete the listbox ARIA pattern in the story.

The container declares role="listbox" and the children declare role="option". The pattern is incomplete:

  • An element with role="option" requires aria-selected. Screen readers announce the state.
  • role="option" on a <button> overrides the native button role, so the element no longer exposes button semantics.
  • The standard listbox pattern makes the container the single tab stop and manages the active option with aria-activedescendant or roving tabindex. Here every option is separately tabbable.

Stories serve as reference usage, so an incomplete pattern gets copied. Either complete the pattern, or drop the listbox and option roles and present the example as a generic focus group.

Also applies to: 362-372

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/focus/stories/focus.stories.tsx` around lines 336 - 337, Update the
focus story’s listbox example around the container and option elements to use a
complete accessible listbox pattern: add option selection state, avoid
overriding native button semantics, and make the container the single tab stop
with active-option management via aria-activedescendant or roving tabindex.
Alternatively, remove the listbox/option roles and present the example as a
generic focus group.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/public-crabs-joke.md:
- Around line 1-5: Update the changeset for `@solid-primitives/focus` from patch
to minor because the exported createFocusGroup primitive adds a new public API;
leave the existing summary unchanged.

In `@packages/focus/README.md`:
- Around line 244-259: Update the README options documentation to distinguish
per-call traversal options (from, tabbable, wrap, and accept) from group-level
keyboard options (orientation, textDirection, handleTab, and
keyboardNavigation). Revise the introductory text and table
headings/descriptions so they no longer claim every option can be passed to
individual methods; do not change implementation behavior.

In `@packages/focus/src/focusGroup.ts`:
- Line 178: Update the TreeWalker result handling in focusFirst so nextNode()
converts null to undefined, matching its HTMLElement | undefined return contract
and the existing focusNext behavior. Preserve the HTMLElement typing while
ensuring empty focus groups return undefined.
- Around line 238-261: Update handleKeyDown so the next, previous, Home, and End
navigation branches only run when no ctrlKey, metaKey, or altKey modifier is
pressed. Preserve the existing Tab handling, including its shiftKey behavior,
and continue preventing default only for unmodified navigation keys.
- Around line 365-387: Update getRadiosInGroup to immediately return [element]
when element.name is empty, before the form and document query branches.
Preserve the existing grouped-radio behavior for non-empty names.
- Around line 325-343: Update the radio check in the walker filtering logic to
use the HTMLInputElement.type property consistently instead of
getAttribute("type"). Preserve the existing isTabbableRadio and same-name
radio-group filtering behavior, including support for case-insensitive or
normalized radio types.

In `@packages/focus/src/tabbable.ts`:
- Around line 84-91: Add an inert-attribute check to isAttributeVisible so
elements with inert are rejected alongside hidden elements. Keep the existing
DETAILS/SUMMARY visibility logic unchanged; ancestor recursion in
isElementVisible will cover descendants of inert containers.

In `@packages/focus/stories/focus.stories.tsx`:
- Line 352: Replace the BoolRow usage for the focused item in the focus story
with StatRow, passing focusedLabel() as its string value so the row uses the
correct type and presentation.

---

Nitpick comments:
In `@packages/focus/src/focusGroup.ts`:
- Around line 108-112: Update the focusNext and focusPrevious walker
construction to pass the from value in the options supplied to
getFocusableTreeWalker, and remove their manual walker.currentNode assignments
so the helper applies its existing from-based positioning and subtree rejection
consistently.
- Around line 312-316: Update the exported getFocusableTreeWalker API by either
documenting the scope parameter in its JSDoc with its intended behavior, or
removing scope and the associated isElementInScope logic until it has a
consumer. Keep the implementation and public signature consistent with the
chosen option.
- Around line 242-253: Optionally update the keydown handling around focusNext,
focusPrevious, focusFirst, and focusLast to detect text-entry targets such as
text inputs, textareas, and content-editable elements, then bypass arrow, Home,
and End focus handling for those targets while preserving existing behavior
elsewhere.
- Around line 38-65: Split FocusGroupOptions into group-level configuration and
a new FocusOptions type containing from, tabbable, wrap, and accept; update
FocusGroup methods to accept FocusOptions so orientation, textDirection,
handleTab, and keyboardNavigation cannot be passed per call. Correct the accept
documentation to describe whether an element is a focus candidate.

In `@packages/focus/src/tabbable.ts`:
- Around line 35-41: Add concise comments above FOCUSABLE_ELEMENT_SELECTOR and
TABBABLE_ELEMENT_SELECTOR documenting that join separators apply guards only
between entries, so the final array entry must include its own hidden/tabindex
exclusions and future entries must preserve those guards.
- Around line 49-56: Keep isElementVisible unchanged for this initial release;
the comment documents a potential performance optimization rather than a
required fix. If optimization becomes necessary, add an ancestor-result cache
scoped to a single getFocusableTreeWalker traversal and reuse it across
candidate visibility checks.

In `@packages/focus/stories/focus.stories.tsx`:
- Around line 336-337: Update the focus story’s listbox example around the
container and option elements to use a complete accessible listbox pattern: add
option selection state, avoid overriding native button semantics, and make the
container the single tab stop with active-option management via
aria-activedescendant or roving tabindex. Alternatively, remove the
listbox/option roles and present the example as a generic focus group.

In `@packages/focus/test/index.test.tsx`:
- Around line 741-746: Remove the unused container parameter from the press
helper and update every call site to pass only the target element and keyboard
event, preserving the existing flush and dispatch behavior.
- Around line 633-636: Wrap every createFocusGroup invocation in the
createFocusGroup describe block with a shared withRoot helper that creates and
disposes a Solid root via onTestFinished. Update the affected tests to call
withRoot(() => createFocusGroup(() => container)), preserving the existing
createFocusGroup behavior and the already-correct keyboard-navigation test.
- Around line 703-734: Expand the focus-group tests to cover radio filtering in
getFocusableTreeWalker, including form and non-form groups, RadioNodeList and
single-element paths, and checked/unchecked behavior via getRadiosInGroup and
isTabbableRadio. Add visibility cases proving hidden and display:none elements
are skipped through isElementVisible, and cover handleTab:false plus
focusPrevious when from is outside the root.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e45826b-d886-4ac5-8994-c4a198175ac9

📥 Commits

Reviewing files that changed from the base of the PR and between 36781ed and 3238a1a.

📒 Files selected for processing (9)
  • .changeset/public-crabs-joke.md
  • packages/focus/README.md
  • packages/focus/package.json
  • packages/focus/src/focusGroup.ts
  • packages/focus/src/index.ts
  • packages/focus/src/tabbable.ts
  • packages/focus/stories/focus.stories.tsx
  • packages/focus/test/index.test.tsx
  • packages/focus/test/server.test.ts

Comment thread .changeset/public-crabs-joke.md
Comment thread packages/focus/README.md
Comment thread packages/focus/src/focusGroup.ts
Comment thread packages/focus/src/focusGroup.ts
Comment thread packages/focus/src/focusGroup.ts
Comment thread packages/focus/src/focusGroup.ts
Comment thread packages/focus/src/tabbable.ts
Comment thread packages/focus/stories/focus.stories.tsx
@davedbase
davedbase merged commit 0e771cb into solidjs-community:next Aug 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants