Skip to content

Commit

Permalink
feat: add reactive controllers package
Browse files Browse the repository at this point in the history
  • Loading branch information
Westbrook committed Jan 21, 2022
1 parent be29bf6 commit d434e9d
Show file tree
Hide file tree
Showing 18 changed files with 843 additions and 15 deletions.
5 changes: 3 additions & 2 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
packages/**/*.d.ts
packages/*/lib/**/*
packages/*/node_modules/**/*
packages/*/node_modules/**/*
tools/**/*.d.ts
tools/*/node_modules/**/*
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,19 @@ projects/**/*.js.map
projects/**/*.d.ts
!projects/*/global.d.ts

tools/**/*.js
tools/**/*.js.map
tools/**/*.d.ts
!tools/*/global.d.ts

# typescript assets
!test/global.d.ts
*.tsbuildinfo

# built css assets
packages/**/*.css.ts
projects/**/*.css.ts
tools/**/*.css.ts
styles/**/*.css.ts

# test assets
Expand Down
8 changes: 4 additions & 4 deletions lerna.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"packages": ["packages/*", "projects/*"],
"packages": ["packages/*", "projects/*", "tools/*"],
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"conventionalCommits": true,
"ignoreChanges": [
"packages/*/test/**",
"packages/*/stories/**",
"**/*.md",
"**/tsconfig.json"
"**/tsconfig.json",
"**/test/**",
"**/stories/**"
]
}
}
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"analyze:quick": "lit-analyzer \"packages/*/src/!(*.css).ts\"",
"build": "yarn build:css && yarn build:ts && yarn build:decorator && yarn build:compare",
"build:clean": "yarn build:css && yarn build:ts:clean && yarn build:decorator && yarn build:compare",
"build:clear-cache": "rimraf packages/*/lib && rimraf packages/*/tsconfig.tsbuildinfo",
"build:clear-cache": "rimraf packages/*/tsconfig.tsbuildinfo && rimraf tools/*/tsconfig.tsbuildinfo",
"build:compare": "tsc --build projects/vrt-compare/tsconfig.json",
"build:component-inventory": "node ./tasks/build-component-inventory.js",
"build:css": "node ./tasks/build-css.js",
Expand Down Expand Up @@ -44,8 +44,8 @@
"lint:css": "stylelint \"packages/**/*.css\"",
"lint:docs": "eslint -f pretty \"projects/documentation/**/*.ts\"",
"lint:js": "pretty-quick --pattern \"tasks/**/*.js\" && pretty-quick --pattern \"scripts/**/*.js\"",
"lint:packagejson": "pretty-quick --pattern package.json && pretty-quick --pattern \"packages/*/package.json\" && pretty-quick --pattern \"projects/*/package.json\"",
"lint:ts": "pretty-quick --pattern \"packages/**/*.ts\" && eslint -f pretty \"packages/**/*.ts\"",
"lint:packagejson": "pretty-quick --pattern package.json && pretty-quick --pattern \"packages/*/package.json\" && pretty-quick --pattern \"projects/*/package.json\" && pretty-quick --pattern \"tools/*/package.json\"",
"lint:ts": "pretty-quick --pattern \"packages/**/*.ts\" && eslint -f pretty \"packages/**/*.ts\" && pretty-quick --pattern \"tools/**/*.ts\" && eslint -f pretty \"tools/**/*.ts\"",
"lint:versions": "node ./scripts/lint-versions.js",
"new-package": "cd projects/templates && plop",
"postcustom-element-json": "lerna exec --ignore \"{@spectrum-web-components/{base,bundle,icons-ui,icons-workflow,iconset,modal,shared,styles},documentation,example-project-rollup,example-project-webpack,swc-templates}\" -- test -f custom-elements.json",
Expand Down Expand Up @@ -182,6 +182,7 @@
"customElements": ".storybook/custom-elements.json",
"workspaces": [
"packages/*",
"projects/*"
"projects/*",
"tools/*"
]
}
3 changes: 3 additions & 0 deletions tasks/build-packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export const buildPackages = async (options) => {
for (const config of await fg(`./packages/*/tsconfig.json`)) {
paths.push(config);
}
for (const config of await fg(`./tools/*/tsconfig.json`)) {
paths.push(config);
}
paths.push('.storybook/tsconfig.json');
buildPackage(paths, options);
};
Expand Down
7 changes: 5 additions & 2 deletions test/tsconfig-test.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
"include": [
"../packages/**/*.ts",
"../packages/*/stories/*.ts",
"../tools/**/*.ts",
"../tools/*/stories/*.ts",
"./testing-helpers.ts",
"./visual/test.ts",
"../global.d.ts"
],
"exclude": [
"../packages/*/lib/**/*",
"../packages/**/node_modules/**/*",
"../packages/*/test/benchmark/*.ts"
"../packages/*/test/benchmark/*.ts",
"../tools/**/node_modules/**/*",
"../tools/*/test/benchmark/*.ts"
]
}
104 changes: 104 additions & 0 deletions tools/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{
"root": true,
"env": {
"browser": true,
"node": false,
"es6": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "notice", "spectrum-web-components"],
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:lit-a11y/recommended"
],
"rules": {
"no-debugger": 2,
"no-console": ["error", { "allow": ["warn", "error"] }],
"spectrum-web-components/prevent-argument-names": [
"error",
["e", "ev", "evt", "err"]
],
"spectrum-web-components/document-active-element": ["error"],
"notice/notice": [
"error",
{
"mustMatch": "Copyright [0-9]{0,4} Adobe. All rights reserved.",
"templateFile": "config/license.js"
}
],
"@typescript-eslint/explicit-function-return-type": [
1,
{
"allowExpressions": true
}
],
"sort-imports": [
"error",
{
"ignoreCase": true,
"ignoreDeclarationSort": true,
"ignoreMemberSort": false,
"allowSeparatedGroups": false
}
],
"lit-a11y/click-events-have-key-events": [
"error",
{
"allowList": [
"sp-button",
"sp-action-button",
"sp-checkbox",
"sp-radio",
"sp-switch",
"sp-menu-item",
"sp-clear-button",
"sp-underlay"
]
}
]
},
"overrides": [
{
"files": ["*.test.ts", "*.stories.ts", "**/benchmark/*.ts"],
"rules": {
"spectrum-web-components/document-active-element": ["off"],
"lit-a11y/no-autofocus": ["off"],
"lit-a11y/tabindex-no-positive": ["off"]
}
},
{
"files": ["**/icons/*.ts", "**/src/elements/*.ts"],
"rules": {
"sort-imports": ["off"]
}
},
{
"files": ["*.stories.ts"],
"rules": {
"no-console": ["off"]
}
},
{
"files": ["Picker.ts", "SplitButton.ts"],
"rules": {
"lit-a11y/click-events-have-key-events": [
"error",
{
"allowList": [
"sp-button",
"sp-action-button",
"sp-checkbox",
"sp-radio",
"sp-switch",
"sp-menu-item",
"sp-clear-button",
"sp-underlay",
"sp-popover"
]
}
]
}
}
]
}
2 changes: 2 additions & 0 deletions tools/reactive-controllers/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
stories
test
7 changes: 7 additions & 0 deletions tools/reactive-controllers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Description

[Reactive controllers](https://lit.dev/docs/composition/controllers/) are a tool for code reuse and composition within [Lit](https://lit.dev), a core dependency of Spectrum Web Components. Reactive controllers can be shared across components to reduce both code complexity and size, and to deliver a consistent user experience. These reactive controllers are used by the Spectrum Web Components library and are published to NPM for you to leverage in your projects as well.

### Reactive controllers

- [RovingTabindexController](../roving-tab-index)
49 changes: 49 additions & 0 deletions tools/reactive-controllers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@spectrum-web-components/reactive-controllers",
"version": "0.0.1",
"publishConfig": {
"access": "public"
},
"description": "ReactiveControllers for powering common UI patterns",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/adobe/spectrum-web-components.git",
"directory": "packages/reactive-controllers"
},
"author": "",
"homepage": "https://adobe.github.io/spectrum-web-components/components/reactive-controllers",
"bugs": {
"url": "https://github.com/adobe/spectrum-web-components/issues"
},
"main": "src/index.js",
"module": "src/index.js",
"type": "module",
"exports": {
".": "./src/index.js",
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"scripts": {
"test": "echo \"Error: run tests from mono-repo root.\" && exit 1"
},
"files": [
"**/*.d.ts",
"**/*.js",
"**/*.js.map",
"!stories/",
"!test/"
],
"keywords": [
"spectrum css",
"web components",
"lit-element",
"lit-html",
"reactive controllers"
],
"dependencies": {
"lit": "^2.0.2",
"tslib": "^2.0.0"
},
"types": "./src/index.d.ts"
}
92 changes: 92 additions & 0 deletions tools/reactive-controllers/roving-tab-index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
## Description

[Roving tabindex](https://www.w3.org/TR/wai-aria-practices-1.2/#kbd_roving_tabindex) in a pattern whereby multiple focusable elements are represented by a single `tabindex=0` element, while the individual elements maintain `tabindex=-1` and are made accessible via arrow keys after the entry element if focused. This allows keyboard users to quickly tab through a page without having to stop on every element in a large collection. Attaching a `RovingTabindexController` to your custom element will manage the supplied `elements` via this pattern.

### Usage

[![See it on NPM!](https://img.shields.io/npm/v/@spectrum-web-components/reactive-controllers?style=for-the-badge)](https://www.npmjs.com/package/@spectrum-web-components/reactive-controllers)
[![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@spectrum-web-components/reactive-controllers?style=for-the-badge)](https://bundlephobia.com/result?p=@spectrum-web-components/reactive-controllers)

```
yarn add @spectrum-web-components/reactive-controllers
```

Import the `RovingTabindexController` via:

```
import { RovingTabindexController } from '@spectrum-web-components/reactive-controllers/RovingTabindex.js';
```

## Example

A `Container` element that manages a collection of `<sp-button>` elements that are slotted into it from outside might look like the following:

```js
import { html, LitElement } from 'lit';
import { RovingTabindexController } from '@spectrum-web-components/reactive-controllers/RovingTabindex.js';
import type { Button } from '@spectrum-web-components/button';

class Container extends LitElement {
rovingTabindexController =
new RovingTabindexController() <
Button >
(this,
{
elements: () => [...this.querySelectorAll('sp-button')],
});

render() {
return html`
<slot></slot>
`;
}
}
```

The above will default to entering the `Container` element via the first `<sp-button>` element every time while making all slotted `<sp-button>` elements accessible via the the arrow key (`ArrowLeft`, `ArrowRight`, `ArrowUp`, and `ArrowDown`) managed tab order.

## Options

A `Container` can further customize the implementation of the `RovingTabindexController` with the following options:

- `direction` to customize how and which arrow keys manage what element is to be focused and accepts a either a string of `both`, `vertical`, `horizontal`, or `grid` or a method returning one of those strings
- `elementEnterAction` enacts actions other than `focus` on the entered element which accepts a method with a signature of `(el: T) => void`
- `elements` provides the elements that will have their `tabindex` managed via a method with a signature of `() => T[]`
- `focusInIndex` to control what element will recieve `tabindex=0` while focus is outside of the `Container` and accepts a method with a signature of `(_elements: T[]) => number`
- `isFocusableElement` describes the state an element much be in to receive `focus` via a method with a signature of `(el: T) => boolean`
- `listenerScope` outlines which parts on a container's DOM when listening for arrow key presses via an element reference or a method returning an element reference with the signature `() => HTMLElement`

## Advanced usage

These options can be combined to form various interfaces from the more default that we saw above to the very complex. Below is another `Container` that manages slotted `<sp-button>` elements via the `RovingTabindexController`. The options provided ensure:

- the first focused `<sp-button>` is the one `selected` by the container
- the elements are only focused via the `ArrowLeft` and `ArrowRight` keys
- when an element is focused it becomes the `selected` element
- only enabled elements are focusable

```js
import { html, LitElement } from 'lit';
import { RovingTabindexController } from '@spectrum-web-components/reactive-controllers/RovingTabindex.js';
import type { Button } from '@spectrum-web-components/button';

class Container extends LitElement {
rovingTabindexController = new RovingTabindexController<Button>(this, {
focusInIndex: (buttons) => return this.selected
? buttons.indexOf(this.selected)
: 0,
direction: 'horizontal',
elementEnterAction: (button) => this.selected = button,
elements: () => [...this.querySelectorAll('sp-button')],
isFocusableElement: (button) => !button.disabled,
});

selected!: Button;

render() {
return html`<slot></slot>`;
}
}
```

The above usage is very close to what can be seen in the [`<sp-radio-group>` element](../components/radio).
Loading

0 comments on commit d434e9d

Please sign in to comment.