Skip to content

Commit

Permalink
[ToggleGroup]: Support @type=multi, allowing multiple toggles to be…
Browse files Browse the repository at this point in the history
… active at once. (#279)

* Support @type=multi toggle

* Patch @glimmer/component's dependencies via glimmerjs/glimmer.js#412
  • Loading branch information
NullVoxPopuli authored Mar 19, 2024
1 parent 8d31814 commit d7fdd05
Show file tree
Hide file tree
Showing 13 changed files with 542 additions and 1,278 deletions.
1 change: 1 addition & 0 deletions docs-app/app/app.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'decorator-transforms/globals';
import './css/styles.css';

import Application from '@ember/application';
Expand Down
9 changes: 8 additions & 1 deletion docs-app/ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ module.exports = function (defaults) {
// Add options here
'ember-cli-babel': {
enableTypeScriptTransform: true,
disableDecoratorTransforms: true,
},
babel: {
plugins: [
// add the new transform.
require.resolve('decorator-transforms'),
],
},
});

Expand Down Expand Up @@ -43,7 +50,7 @@ module.exports = function (defaults) {
// staticEmberSource: true,
packagerOptions: {
webpackConfig: {
devtool: process.env.CI ? 'source-map' : 'eval',
devtool: 'source-map',
resolve: {
alias: {
path: 'path-browserify',
Expand Down
1 change: 1 addition & 0 deletions docs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
},
"dependencies": {
"assert": "^2.0.0",
"decorator-transforms": "^1.1.0",
"docs-api": "workspace:*",
"ember-focus-trap": "^1.1.0",
"ember-headless-form": "^1.0.0",
Expand Down
43 changes: 38 additions & 5 deletions docs-app/public/docs/3-ui/toggle-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,32 @@ import { ToggleGroup } from 'ember-primitives';
<AlignRight />
</t.Item>
</ToggleGroup>
<ToggleGroup @type="multi" class="toggle-group" as |t|>
<t.Item @value="bold" aria-label="Bold text">
B
</t.Item>
<t.Item @value="italic" aria-label="Italicize text">
I
</t.Item>
<t.Item @value="underline" aria-label="Underline text">
U
</t.Item>
</ToggleGroup>
</div>
<style>
.demo { display: flex; justify-content: center; align-items: center;}
button[aria-label="Bold text"] { font-weight: bold; }
button[aria-label="Italicize text"] { font-style: italic; }
button[aria-label="Underline text"] { text-decoration: underline; }
.demo {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
}
.toggle-group {
display: inline-flex;
background-color: #fff;
Expand All @@ -34,7 +56,7 @@ import { ToggleGroup } from 'ember-primitives';
.toggle-group > button {
background-color: white;
color: #fff;
color: #black;
height: 35px;
width: 35px;
display: flex;
Expand Down Expand Up @@ -117,19 +139,30 @@ import { ToggleGroup } from 'ember-primitives';
</template>
```

## API Reference
## API Reference: `@type='single'` (default)

```gjs live no-shadow
import { ComponentSignature } from 'docs-app/docs-support';
<template>
<ComponentSignature @module="components/toggle-group" @name="SingleSignature" />
</template>
```

## API Reference: `@type='multi'`

```gjs live no-shadow
import { ComponentSignature } from 'docs-app/docs-support';
<template>
<ComponentSignature @module="components/toggle-group" @name="ToggleGroup" />
<ComponentSignature @module="components/toggle-group" @name="MultiSignature" />
</template>
```


<hr>

### Item
## API Reference: `Item`

```gjs live no-shadow
import { ComponentSignature } from 'docs-app/docs-support';
Expand Down
7 changes: 7 additions & 0 deletions ember-primitives/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,12 @@ module.exports = {
plugins: ['ember'],
parser: 'ember-eslint-parser',
},

{
files: ['./src/components/toggle-group.gts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
},
],
};
9 changes: 9 additions & 0 deletions ember-primitives/.template-lintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,13 @@ module.exports = {
rules: {
'no-forbidden-elements': 'off',
},
overrides: [
{
files: ['src/components/toggle-group.gts'],
rules: {
// https://github.com/typed-ember/glint/issues/715
'no-args-paths': 'off',
},
},
],
};
1 change: 1 addition & 0 deletions ember-primitives/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"ember-velcro": "^2.1.3",
"reactiveweb": "^1.2.0",
"tabster": "^7.0.1",
"tracked-built-ins": "^3.2.0",
"tracked-toolbox": "^2.0.0"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion ember-primitives/src/components/-private/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*/
export function toggleWithFallback(
uncontrolledToggle: (...args: unknown[]) => void,
controlledToggle?: (...args: unknown[]) => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
controlledToggle?: (...args: any[]) => void,
...args: unknown[]
) {
if (controlledToggle) {
Expand Down
175 changes: 169 additions & 6 deletions ember-primitives/src/components/toggle-group.gts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
import { hash } from '@ember/helper';

import { Types } from 'tabster';
import { TrackedSet } from 'tracked-built-ins';
// The consumer will need to provide types for tracked-toolbox.
// Or.. better yet, we PR to trakcked-toolbox to provide them
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand All @@ -19,7 +21,6 @@ const TABSTER_CONFIG = JSON.stringify({
},
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface ItemSignature<Value = any> {
/**
* The button element will have aria-pressed="true" on it when the button is in the pressed state.
Expand All @@ -45,11 +46,9 @@ export interface ItemSignature<Value = any> {
};
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Item<Value = any> = ComponentLike<ItemSignature<Value>>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class ToggleGroup<Value = any> extends Component<{
export interface SingleSignature<Value> {
Element: HTMLDivElement;
Args: {
/**
Expand All @@ -64,16 +63,136 @@ export class ToggleGroup<Value = any> extends Component<{
*
* When none of the toggles are selected, undefined will be passed.
*/
onChange: (value: Value | undefined) => void;
onChange?: (value: Value | undefined) => void;
};
Blocks: {
default: [
{
/**
* The Toggle Switch
*/
Item: Item;
},
];
};
}> {
}

export interface MultiSignature<Value = any> {
Element: HTMLDivElement;
Args: {
/**
* Optionally set the initial toggle state
*/
value?: Value[] | Set<Value> | Value;
/**
* Callback for when the toggle-group's state is changed.
*
* Can be used to control the state of the component.
*
*
* When none of the toggles are selected, undefined will be passed.
*/
onChange?: (value: Set<Value>) => void;
};
Blocks: {
default: [
{
/**
* The Toggle Switch
*/
Item: Item;
},
];
};
}

interface PrivateSingleSignature<Value = any> {
Element: HTMLDivElement;
Args: {
type?: 'single';

/**
* Optionally set the initial toggle state
*/
value?: Value;
/**
* Callback for when the toggle-group's state is changed.
*
* Can be used to control the state of the component.
*
*
* When none of the toggles are selected, undefined will be passed.
*/
onChange?: (value: Value | undefined) => void;
};
Blocks: {
default: [
{
Item: Item;
},
];
};
}

interface PrivateMultiSignature<Value = any> {
Element: HTMLDivElement;
Args: {
type: 'multi';
/**
* Optionally set the initial toggle state
*/
value?: Value[] | Set<Value> | Value;
/**
* Callback for when the toggle-group's state is changed.
*
* Can be used to control the state of the component.
*
*
* When none of the toggles are selected, undefined will be passed.
*/
onChange?: (value: Set<Value>) => void;
};
Blocks: {
default: [
{
Item: Item;
},
];
};
}

function isMulti(x: 'single' | 'multi' | undefined): x is 'multi' {
return x === 'multi';
}

export class ToggleGroup<Value = any> extends Component<
PrivateSingleSignature<Value> | PrivateMultiSignature<Value>
> {
// See: https://github.com/typed-ember/glint/issues/715
<template>
{{#if (isMulti this.args.type)}}
<MultiToggleGroup
@value={{this.args.value}}
@onChange={{this.args.onChange}}
...attributes
as |x|
>
{{yield x}}
</MultiToggleGroup>
{{else}}
<SingleToggleGroup
@value={{this.args.value}}
@onChange={{this.args.onChange}}
...attributes
as |x|
>
{{yield x}}
</SingleToggleGroup>
{{/if}}
</template>
}

class SingleToggleGroup<Value = any> extends Component<SingleSignature<Value>> {
@localCopy('args.value') activePressed?: Value;

handleToggle = (value: Value) => {
Expand All @@ -96,3 +215,47 @@ export class ToggleGroup<Value = any> extends Component<{
</div>
</template>
}

class MultiToggleGroup<Value = any> extends Component<MultiSignature<Value>> {
/**
* Normalizes @value to a Set
* and makes sure that even if the input Set is reactive,
* we don't mistakenly dirty it.
*/
@cached
get activePressed(): TrackedSet<Value> {
let value = this.args.value;

if (!value) {
return new TrackedSet();
}

if (Array.isArray(value)) {
return new TrackedSet(value);
}

if (value instanceof Set) {
return new TrackedSet(value);
}

return new TrackedSet([value]);
}

handleToggle = (value: Value) => {
if (this.activePressed.has(value)) {
this.activePressed.delete(value);
} else {
this.activePressed.add(value);
}

this.args.onChange?.(new Set<Value>(this.activePressed.values()));
};

isPressed = (value: Value) => this.activePressed.has(value);

<template>
<div data-tabster={{TABSTER_CONFIG}} ...attributes>
{{yield (hash Item=(component Toggle onChange=this.handleToggle isPressed=this.isPressed))}}
</div>
</template>
}
3 changes: 2 additions & 1 deletion ember-primitives/src/components/toggle.gts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// import Component from '@glimmer/component';
import { fn } from '@ember/helper';
import { on } from '@ember/modifier';

Expand Down Expand Up @@ -28,7 +29,7 @@ export interface Signature<Value = any> {
* This can be useful when using the same function for the `@onChange`
* handler with multiple `<Toggle>` components.
*/
onChange?: (value?: Value | undefined) => void;
onChange?: (value: Value | undefined, pressed: boolean) => void;

/**
* When used in a group of Toggles, this option will be helpful to
Expand Down
Loading

0 comments on commit d7fdd05

Please sign in to comment.