Giter Site home page Giter Site logo

danielehrhardt / cmdk Goto Github PK

View Code? Open in Web Editor NEW
0.0 1.0 0.0 826 KB

Fast, composable, unstyled command menu for Angular. Directly inspired from pacocoursey/cmdk

Home Page: http://ngneat.github.io/cmdk

License: MIT License

Shell 0.05% JavaScript 2.29% TypeScript 68.55% HTML 8.49% SCSS 20.63%

cmdk's Introduction


MIT commitizen PRs styled with prettier All Contributors ngneat-lib spectator semantic-release npm

Fast, composable, unstyled command menu for Angular. Directly inspired from pacocoursey/cmdk

@ngneat/cmdk

@ngneat/cmdk is a command menu Angular component that can also be used as an accessible combobox. You render items, it filters and sorts them automatically. @ngneat/cmdk supports a fully composable API, so you can wrap items in other components or even as static HTML.

Demo and examples: ngneat.github.io/cmdk

Features

  • 🎨 Un-styled, so that you can provide your own styles easily
  • πŸ₯™ Provides wrapper, so that you can pass your own template, component or static HTML
  • πŸ” Default filtering present
  • πŸ–ΌοΈ Drop in stylesheet themes provided
  • β™Ώ Accessible

Table of Contents

Installation

Angular CLI

ng add @ngneat/cmdk

NPM

# First, install dependencies
npm install @ngneat/overview @ngneat/until-destroy @angular/cdk

npm install @ngneat/cmdk

Yarn

# First, install dependencies
yarn add @ngneat/overview @ngneat/until-destroy @angular/cdk

yarn add @ngneat/cmdk

Usage

1. Import in module

import { CmdkModule } from '@ngneat/cmdk';

@NgModule({
  imports: [
    CmdkModule,
  ],
})
export class AppModule {}

2. Start using it

<cmdk-command>
  <input cmdkInput />
  <div *cmdkEmpty>No results found.</div>
  <cmdk-list>
    <cmdk-group label="Letters">
      <button cmdkItem>a</button>
      <button cmdkItem>b</button>
      <cmdk-separator></cmdk-separator>
      <button cmdkItem>c</button>
    </cmdk-group>
  </cmdk-list>

  <button cmdkItem>Apple</button>
</cmdk-command>

Components

Each component has a specific class (starting with cmdk-) that can be used for styling.

Command

Render this to show the command menu.

Selector Class
cmdk-command .cmdk-command

Properties

Name Description
@Input() ariaLabel: string Accessible Label for this command menu. Not shown visibly.
@Input() filter?: ((value: string, search: string) => boolean) Custom filter function for whether each command menu item should matches the given search query. It should return a boolean, false being hidden entirely. You can pass null to disable default filtering.
Default: (value, search) => value.toLowerCase().includes(search.toLowerCase())
@Input() value?: string Optional controlled state of the selected command menu item.
@Input() loading?: boolean Optional indicator to show loader
@Input() loop?: boolean Optionally set to true to turn on looping around when using the arrow keys.
@Output() valueChanged: EventEmitter<string> Event handler called when the selected item of the menu changes.

Input

Render this to show the command input.

Selector Class
input[cmdkinput] .cmdk-input

Properties

Name Description
@Input() updateOn: 'blur' | 'change' | 'input' Optional indicator to provide event listener when filtering should happen.
Default: input

List

Contains items and groups.

Selector Class
cmdk-list .cmdk-list

Animate height using the --cmdk-list-height CSS variable.

.cmdk-list {
  min-height: 300px;
  height: var(--cmdk-list-height);
  max-height: 500px;
  transition: height 100ms ease;
}

To scroll item into view earlier near the edges of the viewport, use scroll-padding:

.cmdk-list {
  scroll-padding-block-start: 8px;
  scroll-padding-block-end: 8px;
}

Properties

Name Description
@Input() ariaLabel?: string Accessible Label for this command menu. Not shown visibly.

Item

Item that becomes active on pointer enter. You should provide a unique value for each item, but it will be automatically inferred from the .textContent.

Items will not unmount from the DOM, rather the cmdk-hidden attribute is applied to hide it from view. This may be relevant in your styling.

State Selector Class
Default [cmdkItem] .cmdk-item
Active [cmdkItem][aria-selected] .cmdk-item-active
Filtered [cmdkItem] .cmdk-item-filtered
Disabled [cmdkItem] .cmdk-item-disabled
Hidden (not-filtered) [cmdkItem][cmdk-hidden] ``

Properties

Name Description
value: string | undefined; Contextual Value of the list-item
@Input() disabled: boolean Contextually mark the item as disabled. Keyboard navigation will skip this item.
@Input() filtered: boolean Contextually mark the item as filtered.
@Output() selected: EventEmitter<void> Event handler called when the item is selected

Group

Groups items together with the given label (.cmdk-group-label).

Selector Class
cmdk-group .cmdk-group

Groups will not unmount from the DOM, rather the cmdk-hidden attribute is applied to hide it from view. This may be relevant in your styling.

Properties

Name Description
@Input() label: Content Label for this command group. Can be HTML string
@Input() ariaLabel?: string Accessible Label for this command menu. Not shown visibly.

Empty

Automatically renders when there are no results for the search query.

Selector Class
*cmdkEmpty .cmdk-empty

Loader

This will be conditionally renderer when you pass loading=true with cmdk-command

Selector Class
*cmdkLoader .cmdk-loader

Examples

Code snippets for common use cases.

Nested items

Often selecting one item should navigate deeper, with a more refined set of items. For example selecting "Change theme…" should show new items "Dark theme" and "Light theme". We call these sets of items "pages", and they can be implemented with simple state:

<cmdk-command (keydown)="onKeyDown($event)">
  <input cmdkInput (input)="setSearch($event)" />
  <ng-container *ngIf="!page">
    <button cmdkItem (selected)="setPages('projects')">Search projects...</button>
    <button cmdkItem (selected)="setPages('teams')">Join a team...</button>
  </ng-container>
  <ng-container *ngIf="page === 'projects'">
    <button cmdkItem>Project A</button>
    <button cmdkItem>Project B</button>
  </ng-container>
  <ng-container *ngIf="page === 'teams'">
    <button cmdkItem>Team 1</button>
    <button cmdkItem>Team 2</button>
  </ng-container>
</cmdk-command>
pages: Array<string> = [];
search = '';

get page() {
  return this.pages[this.pages.length - 1];
}

onKeyDown(e: KeyboardEvent) {
  // Escape goes to previous page
  // Backspace goes to previous page when search is empty
  if (e.key === 'Escape' || (e.key === 'Backspace' && !this.search)) {
    e.preventDefault();
    this.pages = this.pages.slice(0, -1);
  }
}

setSearch(ev: Event) {
  this.search = (ev.target as HTMLInputElement)?.value;
}

setPages(page: string) {
  this.pages.push(page);
}

Asynchronous results

Render the items as they become available. Filtering and sorting will happen automatically.

<cmdk-command [loading]="loading">
  <input cmdkInput />
  <div *cmdkLoader>Fetching words...</div>
  <button cmdkItem *ngFor="let item of items" [value]="item">
    {{item}}
  </button>
</cmdk-command>
loading = false;

getItems() {
  this.loading = true;
  setTimeout(() => {
    this.items = ['A', 'B', 'C', 'D'];
    this.loading = false;
  }, 3000);
}

Use inside Popover

We recommend using the Angular CDK Overlay. @ngneat/cdk relies on the Angular CDK, so this will reduce your bundle size a bit due to shared dependencies.

First, configure the trigger component:

<button (click)="isDialogOpen = !isDialogOpen" cdkOverlayOrigin #trigger="cdkOverlayOrigin" [attr.aria-expanded]="isDialogOpen">
  Actions
      <kbd>⌘</kbd>
      <kbd>K</kbd>
</button>
<ng-template
  cdkConnectedOverlay
  [cdkConnectedOverlayOrigin]="trigger"
  [cdkConnectedOverlayOpen]="isDialogOpen"
>
  <app-sub-command-dialog [value]="value"></app-sub-command-dialog>
</ng-template>
isDialogOpen = false;

listener(e: KeyboardEvent) {
  if (e.key === 'k' && (e.metaKey || e.altKey)) {
    e.preventDefault();
    if (this.isDialogOpen) {
      this.isDialogOpen = false;
    } else {
      this.isDialogOpen = true;
    }
  }
}

ngOnInit() {
  document.addEventListener('keydown', (ev) => this.listener(ev));
}

ngOnDestroy() {
  document.removeEventListener('keydown', (ev) => this.listener(ev));
}

Then, render the cmdk-command inside CDK Overlay content:

<div class="cmdk-submenu">
  <cmdk-command>
    <cmdk-list>
      <cmdk-group [label]="value">
        <button cmdkItem *ngFor="let item of items" [value]="item.label">
          {{ item.label }}
        </button>
      </cmdk-group>
    </cmdk-list>
    <input cmdkInput #input placeholder="Search for actions..." />
  </cmdk-command>
</div>
readonly items: Array<{ label: string }> = [
  {
    label: 'Open Application',
  },
  {
    label: 'Show in Finder',
  },
  {
    label: 'Show Info in Finder',
  },
  {
    label: 'Add to Favorites',
  },
];

ngAfterViewInit() {
  this.input.nativeElement.focus();
}

Drop in stylesheets

You can find global stylesheets to drop in as a starting point for styling. See ngneat/cmdk/styles for examples.

You can include the SCSS stylesheet in your application's style file:

// Global is needed for any theme
@use "~@ngneat/cmdk/styles/scss/globals";

// Then add theme
@use "~@ngneat/cmdk/styles/scss/framer";
// @use "~@ngneat/cmdk/styles/scss/vercel";
// @use "~@ngneat/cmdk/styles/scss/linear";
// @use "~@ngneat/cmdk/styles/scss/raycast";

or, use pre-built CSS file in angular.json

// ...
"styles": [
  "...",
  "node_modules/@ngneat/cmdk/styles/globals.css"
  "node_modules/@ngneat/cmdk/styles/framer.css"
],
// ...

FAQ

Accessible? Yes. Labeling, aria attributes, and DOM ordering tested with Voice Over and Chrome DevTools.

Virtualization? No. Good performance up to 2,000-3,000 items, though. Read below to bring your own.

Filter/sort items manually? Yes. Pass filter={yourFilter} to Command. Better memory usage and performance. Bring your own virtualization this way.

Unstyled? Yes, use the listed CSS selectors.

Weird/wrong behavior? Make sure your [cdkItem] has a unique value.

Listen for ⌘K automatically? No, do it yourself to have full control over keybind context.

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Dharmen Shah

️️️️♿️ πŸ’» πŸ–‹ 🎨 πŸ“– πŸ’‘ πŸ€” 🚧 πŸ“¦ πŸ“† πŸ”¬

Netanel Basal

πŸ’¬ πŸ’Ό πŸ” πŸ€” 🚧 πŸ§‘β€πŸ« πŸ“† πŸ”¬ πŸ‘€

Paco

🎨 πŸ“– πŸ€” πŸ”¬

This project follows the all-contributors specification. Contributions of any kind welcome!

cmdk's People

Contributors

danielehrhardt avatar semantic-release-bot avatar shhdharmen avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.