Skip to content

Commit 47b84c2

Browse files
committed
Support @type=multi toggle
1 parent 8d31814 commit 47b84c2

File tree

12 files changed

+535
-1288
lines changed

12 files changed

+535
-1288
lines changed

docs-app/app/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'decorator-transforms/globals';
12
import './css/styles.css';
23

34
import Application from '@ember/application';

docs-app/ember-cli-build.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ module.exports = function (defaults) {
88
// Add options here
99
'ember-cli-babel': {
1010
enableTypeScriptTransform: true,
11+
disableDecoratorTransforms: true,
12+
},
13+
babel: {
14+
plugins: [
15+
// add the new transform.
16+
require.resolve('decorator-transforms'),
17+
],
1118
},
1219
});
1320

@@ -43,7 +50,7 @@ module.exports = function (defaults) {
4350
// staticEmberSource: true,
4451
packagerOptions: {
4552
webpackConfig: {
46-
devtool: process.env.CI ? 'source-map' : 'eval',
53+
devtool: 'source-map',
4754
resolve: {
4855
alias: {
4956
path: 'path-browserify',

docs-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
},
2929
"dependencies": {
3030
"assert": "^2.0.0",
31+
"decorator-transforms": "^1.1.0",
3132
"docs-api": "workspace:*",
3233
"ember-focus-trap": "^1.1.0",
3334
"ember-headless-form": "^1.0.0",

docs-app/public/docs/3-ui/toggle-group.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,32 @@ import { ToggleGroup } from 'ember-primitives';
2121
<AlignRight />
2222
</t.Item>
2323
</ToggleGroup>
24+
25+
<ToggleGroup @type="multi" class="toggle-group" as |t|>
26+
<t.Item @value="bold" aria-label="Bold text">
27+
B
28+
</t.Item>
29+
<t.Item @value="italic" aria-label="Italicize text">
30+
I
31+
</t.Item>
32+
<t.Item @value="underline" aria-label="Underline text">
33+
U
34+
</t.Item>
35+
</ToggleGroup>
2436
</div>
2537
2638
<style>
27-
.demo { display: flex; justify-content: center; align-items: center;}
39+
button[aria-label="Bold text"] { font-weight: bold; }
40+
button[aria-label="Italicize text"] { font-style: italic; }
41+
button[aria-label="Underline text"] { text-decoration: underline; }
42+
43+
.demo {
44+
display: flex;
45+
justify-content: center;
46+
align-items: center;
47+
gap: 1rem;
48+
}
49+
2850
.toggle-group {
2951
display: inline-flex;
3052
background-color: #fff;
@@ -34,7 +56,7 @@ import { ToggleGroup } from 'ember-primitives';
3456
3557
.toggle-group > button {
3658
background-color: white;
37-
color: #fff;
59+
color: #black;
3860
height: 35px;
3961
width: 35px;
4062
display: flex;
@@ -117,19 +139,30 @@ import { ToggleGroup } from 'ember-primitives';
117139
</template>
118140
```
119141

120-
## API Reference
142+
## API Reference: `@type='single'` (default)
143+
144+
```gjs live no-shadow
145+
import { ComponentSignature } from 'docs-app/docs-support';
146+
147+
<template>
148+
<ComponentSignature @module="components/toggle-group" @name="SingleSignature" />
149+
</template>
150+
```
151+
152+
## API Reference: `@type='multi'`
121153

122154
```gjs live no-shadow
123155
import { ComponentSignature } from 'docs-app/docs-support';
124156
125157
<template>
126-
<ComponentSignature @module="components/toggle-group" @name="ToggleGroup" />
158+
<ComponentSignature @module="components/toggle-group" @name="MultiSignature" />
127159
</template>
128160
```
129161

162+
130163
<hr>
131164

132-
### Item
165+
## API Reference: `Item`
133166

134167
```gjs live no-shadow
135168
import { ComponentSignature } from 'docs-app/docs-support';

ember-primitives/.eslintrc.cjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,12 @@ module.exports = {
1919
plugins: ['ember'],
2020
parser: 'ember-eslint-parser',
2121
},
22+
23+
{
24+
files: ['./src/components/toggle-group.gts'],
25+
rules: {
26+
'@typescript-eslint/no-explicit-any': 'off',
27+
},
28+
},
2229
],
2330
};

ember-primitives/.template-lintrc.cjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,13 @@ module.exports = {
55
rules: {
66
'no-forbidden-elements': 'off',
77
},
8+
overrides: [
9+
{
10+
files: ['src/components/toggle-group.gts'],
11+
rules: {
12+
// https://github.com/typed-ember/glint/issues/715
13+
'no-args-paths': 'off',
14+
},
15+
},
16+
],
817
};

ember-primitives/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"ember-velcro": "^2.1.3",
4545
"reactiveweb": "^1.2.0",
4646
"tabster": "^7.0.1",
47+
"tracked-built-ins": "^3.2.0",
4748
"tracked-toolbox": "^2.0.0"
4849
},
4950
"devDependencies": {

ember-primitives/src/components/-private/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
*/
55
export function toggleWithFallback(
66
uncontrolledToggle: (...args: unknown[]) => void,
7-
controlledToggle?: (...args: unknown[]) => void,
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
controlledToggle?: (...args: any[]) => void,
89
...args: unknown[]
910
) {
1011
if (controlledToggle) {

ember-primitives/src/components/toggle-group.gts

Lines changed: 169 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import Component from '@glimmer/component';
2+
import { cached } from '@glimmer/tracking';
23
import { hash } from '@ember/helper';
34

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

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

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

51-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
52-
export class ToggleGroup<Value = any> extends Component<{
51+
export interface SingleSignature<Value> {
5352
Element: HTMLDivElement;
5453
Args: {
5554
/**
@@ -64,16 +63,136 @@ export class ToggleGroup<Value = any> extends Component<{
6463
*
6564
* When none of the toggles are selected, undefined will be passed.
6665
*/
67-
onChange: (value: Value | undefined) => void;
66+
onChange?: (value: Value | undefined) => void;
6867
};
6968
Blocks: {
7069
default: [
7170
{
71+
/**
72+
* The Toggle Switch
73+
*/
7274
Item: Item;
7375
},
7476
];
7577
};
76-
}> {
78+
}
79+
80+
export interface MultiSignature<Value = any> {
81+
Element: HTMLDivElement;
82+
Args: {
83+
/**
84+
* Optionally set the initial toggle state
85+
*/
86+
value?: Value[] | Set<Value> | Value;
87+
/**
88+
* Callback for when the toggle-group's state is changed.
89+
*
90+
* Can be used to control the state of the component.
91+
*
92+
*
93+
* When none of the toggles are selected, undefined will be passed.
94+
*/
95+
onChange?: (value: Set<Value>) => void;
96+
};
97+
Blocks: {
98+
default: [
99+
{
100+
/**
101+
* The Toggle Switch
102+
*/
103+
Item: Item;
104+
},
105+
];
106+
};
107+
}
108+
109+
interface PrivateSingleSignature<Value = any> {
110+
Element: HTMLDivElement;
111+
Args: {
112+
type?: 'single';
113+
114+
/**
115+
* Optionally set the initial toggle state
116+
*/
117+
value?: Value;
118+
/**
119+
* Callback for when the toggle-group's state is changed.
120+
*
121+
* Can be used to control the state of the component.
122+
*
123+
*
124+
* When none of the toggles are selected, undefined will be passed.
125+
*/
126+
onChange?: (value: Value | undefined) => void;
127+
};
128+
Blocks: {
129+
default: [
130+
{
131+
Item: Item;
132+
},
133+
];
134+
};
135+
}
136+
137+
interface PrivateMultiSignature<Value = any> {
138+
Element: HTMLDivElement;
139+
Args: {
140+
type: 'multi';
141+
/**
142+
* Optionally set the initial toggle state
143+
*/
144+
value?: Value[] | Set<Value> | Value;
145+
/**
146+
* Callback for when the toggle-group's state is changed.
147+
*
148+
* Can be used to control the state of the component.
149+
*
150+
*
151+
* When none of the toggles are selected, undefined will be passed.
152+
*/
153+
onChange?: (value: Set<Value>) => void;
154+
};
155+
Blocks: {
156+
default: [
157+
{
158+
Item: Item;
159+
},
160+
];
161+
};
162+
}
163+
164+
function isMulti(x: 'single' | 'multi' | undefined): x is 'multi' {
165+
return x === 'multi';
166+
}
167+
168+
export class ToggleGroup<Value = any> extends Component<
169+
PrivateSingleSignature<Value> | PrivateMultiSignature<Value>
170+
> {
171+
// See: https://github.com/typed-ember/glint/issues/715
172+
<template>
173+
{{#if (isMulti this.args.type)}}
174+
<MultiToggleGroup
175+
@value={{this.args.value}}
176+
@onChange={{this.args.onChange}}
177+
...attributes
178+
as |x|
179+
>
180+
{{yield x}}
181+
</MultiToggleGroup>
182+
{{else}}
183+
<SingleToggleGroup
184+
@value={{this.args.value}}
185+
@onChange={{this.args.onChange}}
186+
...attributes
187+
as |x|
188+
>
189+
{{yield x}}
190+
</SingleToggleGroup>
191+
{{/if}}
192+
</template>
193+
}
194+
195+
class SingleToggleGroup<Value = any> extends Component<SingleSignature<Value>> {
77196
@localCopy('args.value') activePressed?: Value;
78197

79198
handleToggle = (value: Value) => {
@@ -96,3 +215,47 @@ export class ToggleGroup<Value = any> extends Component<{
96215
</div>
97216
</template>
98217
}
218+
219+
class MultiToggleGroup<Value = any> extends Component<MultiSignature<Value>> {
220+
/**
221+
* Normalizes @value to a Set
222+
* and makes sure that even if the input Set is reactive,
223+
* we don't mistakenly dirty it.
224+
*/
225+
@cached
226+
get activePressed(): TrackedSet<Value> {
227+
let value = this.args.value;
228+
229+
if (!value) {
230+
return new TrackedSet();
231+
}
232+
233+
if (Array.isArray(value)) {
234+
return new TrackedSet(value);
235+
}
236+
237+
if (value instanceof Set) {
238+
return new TrackedSet(value);
239+
}
240+
241+
return new TrackedSet([value]);
242+
}
243+
244+
handleToggle = (value: Value) => {
245+
if (this.activePressed.has(value)) {
246+
this.activePressed.delete(value);
247+
} else {
248+
this.activePressed.add(value);
249+
}
250+
251+
this.args.onChange?.(new Set<Value>(this.activePressed.values()));
252+
};
253+
254+
isPressed = (value: Value) => this.activePressed.has(value);
255+
256+
<template>
257+
<div data-tabster={{TABSTER_CONFIG}} ...attributes>
258+
{{yield (hash Item=(component Toggle onChange=this.handleToggle isPressed=this.isPressed))}}
259+
</div>
260+
</template>
261+
}

ember-primitives/src/components/toggle.gts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// import Component from '@glimmer/component';
12
import { fn } from '@ember/helper';
23
import { on } from '@ember/modifier';
34

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

3334
/**
3435
* When used in a group of Toggles, this option will be helpful to

0 commit comments

Comments
 (0)