@furystack/shades-common-components 13.1.0 → 13.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/esm/components/app-bar.d.ts.map +1 -1
- package/esm/components/app-bar.js +12 -3
- package/esm/components/app-bar.js.map +1 -1
- package/esm/components/avatar.d.ts.map +1 -1
- package/esm/components/avatar.js +3 -5
- package/esm/components/avatar.js.map +1 -1
- package/esm/components/command-palette/command-palette-input.d.ts +1 -2
- package/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
- package/esm/components/command-palette/command-palette-input.js +14 -36
- package/esm/components/command-palette/command-palette-input.js.map +1 -1
- package/esm/components/command-palette/command-palette-input.spec.js +14 -116
- package/esm/components/command-palette/command-palette-input.spec.js.map +1 -1
- package/esm/components/command-palette/index.d.ts.map +1 -1
- package/esm/components/command-palette/index.js +3 -0
- package/esm/components/command-palette/index.js.map +1 -1
- package/esm/components/drawer/index.d.ts.map +1 -1
- package/esm/components/drawer/index.js +4 -0
- package/esm/components/drawer/index.js.map +1 -1
- package/esm/components/drawer/index.spec.js +47 -0
- package/esm/components/drawer/index.spec.js.map +1 -1
- package/esm/components/noty-list.d.ts.map +1 -1
- package/esm/components/noty-list.js +1 -3
- package/esm/components/noty-list.js.map +1 -1
- package/esm/services/css-variable-theme.d.ts +1 -1
- package/esm/services/css-variable-theme.d.ts.map +1 -1
- package/esm/services/css-variable-theme.js +5 -5
- package/esm/services/css-variable-theme.js.map +1 -1
- package/esm/services/css-variable-theme.spec.js +29 -1
- package/esm/services/css-variable-theme.spec.js.map +1 -1
- package/esm/services/layout-service.d.ts +8 -0
- package/esm/services/layout-service.d.ts.map +1 -1
- package/esm/services/layout-service.js +16 -0
- package/esm/services/layout-service.js.map +1 -1
- package/esm/services/layout-service.spec.js +55 -0
- package/esm/services/layout-service.spec.js.map +1 -1
- package/esm/services/theme-provider-service.d.ts +11 -10
- package/esm/services/theme-provider-service.d.ts.map +1 -1
- package/esm/services/theme-provider-service.js +3 -2
- package/esm/services/theme-provider-service.js.map +1 -1
- package/esm/services/theme-provider-service.spec.js +35 -1
- package/esm/services/theme-provider-service.spec.js.map +1 -1
- package/esm/themes/architect-theme.d.ts +1 -0
- package/esm/themes/architect-theme.d.ts.map +1 -1
- package/esm/themes/architect-theme.js +1 -0
- package/esm/themes/architect-theme.js.map +1 -1
- package/esm/themes/auditore-theme.d.ts +1 -0
- package/esm/themes/auditore-theme.d.ts.map +1 -1
- package/esm/themes/auditore-theme.js +1 -0
- package/esm/themes/auditore-theme.js.map +1 -1
- package/esm/themes/black-mesa-theme.d.ts +1 -0
- package/esm/themes/black-mesa-theme.d.ts.map +1 -1
- package/esm/themes/black-mesa-theme.js +1 -0
- package/esm/themes/black-mesa-theme.js.map +1 -1
- package/esm/themes/default-dark-theme.d.ts +1 -0
- package/esm/themes/default-dark-theme.d.ts.map +1 -1
- package/esm/themes/default-dark-theme.js +1 -0
- package/esm/themes/default-dark-theme.js.map +1 -1
- package/esm/themes/default-light-theme.d.ts +1 -0
- package/esm/themes/default-light-theme.d.ts.map +1 -1
- package/esm/themes/default-light-theme.js +1 -0
- package/esm/themes/default-light-theme.js.map +1 -1
- package/esm/themes/dragonborn-theme.d.ts +1 -0
- package/esm/themes/dragonborn-theme.d.ts.map +1 -1
- package/esm/themes/dragonborn-theme.js +1 -0
- package/esm/themes/dragonborn-theme.js.map +1 -1
- package/esm/themes/hawkins-theme.d.ts +1 -0
- package/esm/themes/hawkins-theme.d.ts.map +1 -1
- package/esm/themes/hawkins-theme.js +1 -0
- package/esm/themes/hawkins-theme.js.map +1 -1
- package/esm/themes/jedi-theme.d.ts +1 -0
- package/esm/themes/jedi-theme.d.ts.map +1 -1
- package/esm/themes/jedi-theme.js +1 -0
- package/esm/themes/jedi-theme.js.map +1 -1
- package/esm/themes/neon-runner-theme.d.ts +1 -0
- package/esm/themes/neon-runner-theme.d.ts.map +1 -1
- package/esm/themes/neon-runner-theme.js +1 -0
- package/esm/themes/neon-runner-theme.js.map +1 -1
- package/esm/themes/plumber-theme.d.ts +1 -0
- package/esm/themes/plumber-theme.d.ts.map +1 -1
- package/esm/themes/plumber-theme.js +1 -0
- package/esm/themes/plumber-theme.js.map +1 -1
- package/esm/themes/replicant-theme.d.ts +1 -0
- package/esm/themes/replicant-theme.d.ts.map +1 -1
- package/esm/themes/replicant-theme.js +1 -0
- package/esm/themes/replicant-theme.js.map +1 -1
- package/esm/themes/sandworm-theme.d.ts +1 -0
- package/esm/themes/sandworm-theme.d.ts.map +1 -1
- package/esm/themes/sandworm-theme.js +1 -0
- package/esm/themes/sandworm-theme.js.map +1 -1
- package/esm/themes/shadow-broker-theme.d.ts +1 -0
- package/esm/themes/shadow-broker-theme.d.ts.map +1 -1
- package/esm/themes/shadow-broker-theme.js +1 -0
- package/esm/themes/shadow-broker-theme.js.map +1 -1
- package/esm/themes/sith-theme.d.ts +1 -0
- package/esm/themes/sith-theme.d.ts.map +1 -1
- package/esm/themes/sith-theme.js +1 -0
- package/esm/themes/sith-theme.js.map +1 -1
- package/esm/themes/vault-dweller-theme.d.ts +1 -0
- package/esm/themes/vault-dweller-theme.d.ts.map +1 -1
- package/esm/themes/vault-dweller-theme.js +1 -0
- package/esm/themes/vault-dweller-theme.js.map +1 -1
- package/esm/themes/wild-hunt-theme.d.ts +1 -0
- package/esm/themes/wild-hunt-theme.d.ts.map +1 -1
- package/esm/themes/wild-hunt-theme.js +1 -0
- package/esm/themes/wild-hunt-theme.js.map +1 -1
- package/esm/themes/xenomorph-theme.d.ts +1 -0
- package/esm/themes/xenomorph-theme.d.ts.map +1 -1
- package/esm/themes/xenomorph-theme.js +1 -0
- package/esm/themes/xenomorph-theme.js.map +1 -1
- package/package.json +1 -1
- package/src/components/app-bar.tsx +12 -3
- package/src/components/avatar.tsx +20 -5
- package/src/components/command-palette/command-palette-input.spec.tsx +14 -156
- package/src/components/command-palette/command-palette-input.tsx +13 -45
- package/src/components/command-palette/index.tsx +4 -0
- package/src/components/drawer/index.spec.tsx +64 -0
- package/src/components/drawer/index.tsx +5 -0
- package/src/components/noty-list.tsx +1 -3
- package/src/services/css-variable-theme.spec.ts +43 -1
- package/src/services/css-variable-theme.ts +5 -5
- package/src/services/layout-service.spec.ts +74 -0
- package/src/services/layout-service.ts +18 -0
- package/src/services/theme-provider-service.spec.ts +49 -1
- package/src/services/theme-provider-service.ts +12 -11
- package/src/themes/architect-theme.ts +1 -0
- package/src/themes/auditore-theme.ts +1 -0
- package/src/themes/black-mesa-theme.ts +1 -0
- package/src/themes/default-dark-theme.ts +1 -0
- package/src/themes/default-light-theme.ts +1 -0
- package/src/themes/dragonborn-theme.ts +1 -0
- package/src/themes/hawkins-theme.ts +1 -0
- package/src/themes/jedi-theme.ts +1 -0
- package/src/themes/neon-runner-theme.ts +1 -0
- package/src/themes/plumber-theme.ts +1 -0
- package/src/themes/replicant-theme.ts +1 -0
- package/src/themes/sandworm-theme.ts +1 -0
- package/src/themes/shadow-broker-theme.ts +1 -0
- package/src/themes/sith-theme.ts +1 -0
- package/src/themes/vault-dweller-theme.ts +1 -0
- package/src/themes/wild-hunt-theme.ts +1 -0
- package/src/themes/xenomorph-theme.ts +1 -0
|
@@ -6,42 +6,14 @@ import { CommandPaletteInput } from './command-palette-input.js'
|
|
|
6
6
|
import { CommandPaletteManager } from './command-palette-manager.js'
|
|
7
7
|
|
|
8
8
|
describe('CommandPaletteInput', () => {
|
|
9
|
-
let originalAnimate: typeof Element.prototype.animate
|
|
10
|
-
let animateCalls: Array<{ keyframes: unknown; options: unknown }>
|
|
11
|
-
|
|
12
9
|
beforeEach(() => {
|
|
13
10
|
vi.useFakeTimers()
|
|
14
11
|
document.body.innerHTML = '<div id="root"></div>'
|
|
15
|
-
animateCalls = []
|
|
16
|
-
originalAnimate = Element.prototype.animate
|
|
17
|
-
|
|
18
|
-
Element.prototype.animate = vi.fn(
|
|
19
|
-
(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions) => {
|
|
20
|
-
animateCalls.push({ keyframes, options })
|
|
21
|
-
const mockAnimation = {
|
|
22
|
-
onfinish: null as ((event: AnimationPlaybackEvent) => void) | null,
|
|
23
|
-
oncancel: null as ((event: AnimationPlaybackEvent) => void) | null,
|
|
24
|
-
cancel: vi.fn(),
|
|
25
|
-
play: vi.fn(),
|
|
26
|
-
pause: vi.fn(),
|
|
27
|
-
finish: vi.fn(),
|
|
28
|
-
addEventListener: vi.fn(),
|
|
29
|
-
removeEventListener: vi.fn(),
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
setTimeout(() => {
|
|
33
|
-
mockAnimation.onfinish?.({} as AnimationPlaybackEvent)
|
|
34
|
-
}, 10)
|
|
35
|
-
|
|
36
|
-
return mockAnimation as unknown as Animation
|
|
37
|
-
},
|
|
38
|
-
) as typeof Element.prototype.animate
|
|
39
12
|
})
|
|
40
13
|
|
|
41
14
|
afterEach(() => {
|
|
42
15
|
vi.useRealTimers()
|
|
43
16
|
document.body.innerHTML = ''
|
|
44
|
-
Element.prototype.animate = originalAnimate
|
|
45
17
|
vi.restoreAllMocks()
|
|
46
18
|
})
|
|
47
19
|
|
|
@@ -89,7 +61,7 @@ describe('CommandPaletteInput', () => {
|
|
|
89
61
|
})
|
|
90
62
|
})
|
|
91
63
|
|
|
92
|
-
it('should
|
|
64
|
+
it('should always have width 100%', async () => {
|
|
93
65
|
await usingAsync(new Injector(), async (injector) => {
|
|
94
66
|
await usingAsync(createManager(), async (manager) => {
|
|
95
67
|
manager.isOpened.setValue(false)
|
|
@@ -104,15 +76,15 @@ describe('CommandPaletteInput', () => {
|
|
|
104
76
|
await flushUpdates()
|
|
105
77
|
|
|
106
78
|
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
107
|
-
|
|
79
|
+
const computedStyle = window.getComputedStyle(component)
|
|
80
|
+
expect(computedStyle.width).toBe('100%')
|
|
108
81
|
})
|
|
109
82
|
})
|
|
110
83
|
})
|
|
111
84
|
|
|
112
|
-
it('should have
|
|
85
|
+
it('should have overflow hidden style', async () => {
|
|
113
86
|
await usingAsync(new Injector(), async (injector) => {
|
|
114
87
|
await usingAsync(createManager(), async (manager) => {
|
|
115
|
-
manager.isOpened.setValue(true)
|
|
116
88
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
117
89
|
|
|
118
90
|
initializeShadeRoot({
|
|
@@ -124,73 +96,13 @@ describe('CommandPaletteInput', () => {
|
|
|
124
96
|
await flushUpdates()
|
|
125
97
|
|
|
126
98
|
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('should animate width when opening', async () => {
|
|
133
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
134
|
-
await usingAsync(createManager(), async (manager) => {
|
|
135
|
-
manager.isOpened.setValue(false)
|
|
136
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
137
|
-
|
|
138
|
-
initializeShadeRoot({
|
|
139
|
-
injector,
|
|
140
|
-
rootElement,
|
|
141
|
-
jsxElement: <CommandPaletteInput manager={manager} />,
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
await flushUpdates()
|
|
145
|
-
animateCalls = []
|
|
146
|
-
|
|
147
|
-
manager.isOpened.setValue(true)
|
|
148
|
-
await flushUpdates()
|
|
149
|
-
|
|
150
|
-
const widthAnimation = animateCalls.find(
|
|
151
|
-
(call) =>
|
|
152
|
-
Array.isArray(call.keyframes) &&
|
|
153
|
-
call.keyframes.some((kf: Keyframe) => kf.width === '0%') &&
|
|
154
|
-
call.keyframes.some((kf: Keyframe) => kf.width === '100%'),
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
expect(widthAnimation).toBeDefined()
|
|
158
|
-
expect((widthAnimation?.options as KeyframeAnimationOptions)?.duration).toBe(300)
|
|
159
|
-
})
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('should animate width when closing', async () => {
|
|
164
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
165
|
-
await usingAsync(createManager(), async (manager) => {
|
|
166
|
-
manager.isOpened.setValue(true)
|
|
167
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
168
|
-
|
|
169
|
-
initializeShadeRoot({
|
|
170
|
-
injector,
|
|
171
|
-
rootElement,
|
|
172
|
-
jsxElement: <CommandPaletteInput manager={manager} />,
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
await flushUpdates()
|
|
176
|
-
animateCalls = []
|
|
177
|
-
|
|
178
|
-
manager.isOpened.setValue(false)
|
|
179
|
-
await flushUpdates()
|
|
180
|
-
|
|
181
|
-
const widthAnimation = animateCalls.find(
|
|
182
|
-
(call) =>
|
|
183
|
-
Array.isArray(call.keyframes) &&
|
|
184
|
-
call.keyframes.some((kf: Keyframe) => kf.width === '100%') &&
|
|
185
|
-
call.keyframes.some((kf: Keyframe) => kf.width === '0%'),
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
expect(widthAnimation).toBeDefined()
|
|
99
|
+
const computedStyle = window.getComputedStyle(component)
|
|
100
|
+
expect(computedStyle.overflow).toBe('hidden')
|
|
189
101
|
})
|
|
190
102
|
})
|
|
191
103
|
})
|
|
192
104
|
|
|
193
|
-
it('should
|
|
105
|
+
it('should focus input when opened', async () => {
|
|
194
106
|
await usingAsync(new Injector(), async (injector) => {
|
|
195
107
|
await usingAsync(createManager(), async (manager) => {
|
|
196
108
|
manager.isOpened.setValue(false)
|
|
@@ -206,12 +118,12 @@ describe('CommandPaletteInput', () => {
|
|
|
206
118
|
|
|
207
119
|
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
208
120
|
const inputElement = component?.querySelector('input') as HTMLInputElement
|
|
209
|
-
|
|
121
|
+
const focusSpy = vi.spyOn(inputElement, 'focus')
|
|
210
122
|
|
|
211
123
|
manager.isOpened.setValue(true)
|
|
212
124
|
await flushUpdates()
|
|
213
125
|
|
|
214
|
-
expect(
|
|
126
|
+
expect(focusSpy).toHaveBeenCalled()
|
|
215
127
|
})
|
|
216
128
|
})
|
|
217
129
|
})
|
|
@@ -236,35 +148,13 @@ describe('CommandPaletteInput', () => {
|
|
|
236
148
|
|
|
237
149
|
manager.isOpened.setValue(false)
|
|
238
150
|
await flushUpdates()
|
|
239
|
-
await vi.advanceTimersByTimeAsync(20)
|
|
240
|
-
await flushUpdates()
|
|
241
151
|
|
|
242
152
|
expect(inputElement.value).toBe('')
|
|
243
153
|
})
|
|
244
154
|
})
|
|
245
155
|
})
|
|
246
156
|
|
|
247
|
-
it('should
|
|
248
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
249
|
-
await usingAsync(createManager(), async (manager) => {
|
|
250
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
251
|
-
|
|
252
|
-
initializeShadeRoot({
|
|
253
|
-
injector,
|
|
254
|
-
rootElement,
|
|
255
|
-
jsxElement: <CommandPaletteInput manager={manager} />,
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
await flushUpdates()
|
|
259
|
-
|
|
260
|
-
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
261
|
-
const computedStyle = window.getComputedStyle(component)
|
|
262
|
-
expect(computedStyle.overflow).toBe('hidden')
|
|
263
|
-
})
|
|
264
|
-
})
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('should use cubic-bezier easing for animations', async () => {
|
|
157
|
+
it('should preserve input value when opening', async () => {
|
|
268
158
|
await usingAsync(new Injector(), async (injector) => {
|
|
269
159
|
await usingAsync(createManager(), async (manager) => {
|
|
270
160
|
manager.isOpened.setValue(false)
|
|
@@ -277,47 +167,15 @@ describe('CommandPaletteInput', () => {
|
|
|
277
167
|
})
|
|
278
168
|
|
|
279
169
|
await flushUpdates()
|
|
280
|
-
animateCalls = []
|
|
281
170
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const widthAnimation = animateCalls.find(
|
|
286
|
-
(call) => Array.isArray(call.keyframes) && call.keyframes.some((kf: Keyframe) => 'width' in kf),
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
expect(widthAnimation).toBeDefined()
|
|
290
|
-
expect((widthAnimation?.options as KeyframeAnimationOptions)?.easing).toBe(
|
|
291
|
-
'cubic-bezier(0.595, 0.425, 0.415, 0.845)',
|
|
292
|
-
)
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
it('should fill animation forwards', async () => {
|
|
298
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
299
|
-
await usingAsync(createManager(), async (manager) => {
|
|
300
|
-
manager.isOpened.setValue(false)
|
|
301
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
302
|
-
|
|
303
|
-
initializeShadeRoot({
|
|
304
|
-
injector,
|
|
305
|
-
rootElement,
|
|
306
|
-
jsxElement: <CommandPaletteInput manager={manager} />,
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
await flushUpdates()
|
|
310
|
-
animateCalls = []
|
|
171
|
+
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
172
|
+
const inputElement = component?.querySelector('input') as HTMLInputElement
|
|
173
|
+
inputElement.value = 'some text'
|
|
311
174
|
|
|
312
175
|
manager.isOpened.setValue(true)
|
|
313
176
|
await flushUpdates()
|
|
314
177
|
|
|
315
|
-
|
|
316
|
-
(call) => Array.isArray(call.keyframes) && call.keyframes.some((kf: Keyframe) => 'width' in kf),
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
expect(widthAnimation).toBeDefined()
|
|
320
|
-
expect((widthAnimation?.options as KeyframeAnimationOptions)?.fill).toBe('forwards')
|
|
178
|
+
expect(inputElement.value).toBe('some text')
|
|
321
179
|
})
|
|
322
180
|
})
|
|
323
181
|
})
|
|
@@ -1,45 +1,13 @@
|
|
|
1
|
-
import type { RefObject } from '@furystack/shades'
|
|
2
1
|
import { Shade, createComponent } from '@furystack/shades'
|
|
3
2
|
import { cssVariableTheme } from '../../services/css-variable-theme.js'
|
|
4
|
-
import { promisifyAnimation } from '../../utils/promisify-animation.js'
|
|
5
3
|
import type { CommandPaletteManager } from './command-palette-manager.js'
|
|
6
4
|
|
|
7
|
-
const animateOpenState = async (
|
|
8
|
-
wrapperRef: RefObject<HTMLDivElement>,
|
|
9
|
-
inputRef: RefObject<HTMLInputElement>,
|
|
10
|
-
isOpened: boolean,
|
|
11
|
-
) => {
|
|
12
|
-
const wrapper = wrapperRef.current
|
|
13
|
-
const input = inputRef.current
|
|
14
|
-
if (wrapper && input) {
|
|
15
|
-
if (isOpened) {
|
|
16
|
-
input.value = ''
|
|
17
|
-
await promisifyAnimation(wrapper, [{ width: '0%' }, { width: '100%' }], {
|
|
18
|
-
duration: 300,
|
|
19
|
-
fill: 'forwards',
|
|
20
|
-
easing: 'cubic-bezier(0.595, 0.425, 0.415, 0.845)',
|
|
21
|
-
})
|
|
22
|
-
input.focus()
|
|
23
|
-
} else {
|
|
24
|
-
await promisifyAnimation(wrapper, [{ width: '100%' }, { width: '0%' }], {
|
|
25
|
-
duration: 300,
|
|
26
|
-
fill: 'forwards',
|
|
27
|
-
easing: 'cubic-bezier(0.595, 0.425, 0.415, 0.845)',
|
|
28
|
-
})
|
|
29
|
-
input.value = ''
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
5
|
export const CommandPaletteInput = Shade<{ manager: CommandPaletteManager }>({
|
|
35
6
|
shadowDomName: 'shades-command-palette-input',
|
|
36
7
|
css: {
|
|
37
|
-
width: '
|
|
8
|
+
width: '100%',
|
|
38
9
|
fontFamily: cssVariableTheme.typography.fontFamily,
|
|
39
10
|
overflow: 'hidden',
|
|
40
|
-
'&[data-opened]': {
|
|
41
|
-
width: '100%',
|
|
42
|
-
},
|
|
43
11
|
'& input': {
|
|
44
12
|
color: cssVariableTheme.text.primary,
|
|
45
13
|
outline: 'none',
|
|
@@ -53,20 +21,20 @@ export const CommandPaletteInput = Shade<{ manager: CommandPaletteManager }>({
|
|
|
53
21
|
letterSpacing: '0.01em',
|
|
54
22
|
},
|
|
55
23
|
},
|
|
56
|
-
render: ({ props, useObservable, useRef
|
|
57
|
-
const { manager } = props
|
|
58
|
-
const wrapperRef = useRef<HTMLDivElement>('wrapper')
|
|
24
|
+
render: ({ props, useObservable, useRef }) => {
|
|
59
25
|
const inputRef = useRef<HTMLInputElement>('input')
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
26
|
+
useObservable('isOpened', props.manager.isOpened, {
|
|
27
|
+
onChange: (isOpened) => {
|
|
28
|
+
if (inputRef.current) {
|
|
29
|
+
if (isOpened) {
|
|
30
|
+
inputRef.current.focus()
|
|
31
|
+
} else {
|
|
32
|
+
inputRef.current.value = ''
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
63
36
|
})
|
|
64
|
-
useHostProps({ ...(isCurrentlyOpened ? { 'data-opened': '' } : {}) })
|
|
65
37
|
|
|
66
|
-
return
|
|
67
|
-
<div ref={wrapperRef} style={{ width: isCurrentlyOpened ? '100%' : '0%', overflow: 'hidden' }}>
|
|
68
|
-
<input ref={inputRef} autofocus placeholder="Type to search commands..." />
|
|
69
|
-
</div>
|
|
70
|
-
)
|
|
38
|
+
return <input ref={inputRef} autofocus placeholder="Type to search commands..." />
|
|
71
39
|
},
|
|
72
40
|
})
|
|
@@ -718,6 +718,70 @@ describe('Drawer component', () => {
|
|
|
718
718
|
})
|
|
719
719
|
})
|
|
720
720
|
|
|
721
|
+
describe('cleanup on disposal', () => {
|
|
722
|
+
it('should call removeDrawer on LayoutService when the component is removed from DOM', async () => {
|
|
723
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
724
|
+
const layoutService = new LayoutService(createMockElement())
|
|
725
|
+
injector.setExplicitInstance(layoutService, LayoutService)
|
|
726
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
727
|
+
|
|
728
|
+
initializeShadeRoot({
|
|
729
|
+
injector,
|
|
730
|
+
rootElement,
|
|
731
|
+
jsxElement: (
|
|
732
|
+
<Drawer position="left" variant="collapsible">
|
|
733
|
+
<div>Drawer</div>
|
|
734
|
+
</Drawer>
|
|
735
|
+
),
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
await flushUpdates()
|
|
739
|
+
expect(layoutService.drawerState.getValue().left).toBeDefined()
|
|
740
|
+
|
|
741
|
+
const removeDrawerSpy = vi.spyOn(layoutService, 'removeDrawer')
|
|
742
|
+
const drawer = document.querySelector('shade-drawer') as HTMLElement
|
|
743
|
+
drawer.remove()
|
|
744
|
+
await flushUpdates()
|
|
745
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
746
|
+
|
|
747
|
+
expect(removeDrawerSpy).toHaveBeenCalledWith('left')
|
|
748
|
+
})
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
it('should only clean up its own drawer position on disposal', async () => {
|
|
752
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
753
|
+
const layoutService = new LayoutService(createMockElement())
|
|
754
|
+
injector.setExplicitInstance(layoutService, LayoutService)
|
|
755
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
756
|
+
|
|
757
|
+
layoutService.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
|
|
758
|
+
|
|
759
|
+
initializeShadeRoot({
|
|
760
|
+
injector,
|
|
761
|
+
rootElement,
|
|
762
|
+
jsxElement: (
|
|
763
|
+
<Drawer position="right" variant="temporary">
|
|
764
|
+
<div>Right Drawer</div>
|
|
765
|
+
</Drawer>
|
|
766
|
+
),
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
await flushUpdates()
|
|
770
|
+
expect(layoutService.drawerState.getValue().right).toBeDefined()
|
|
771
|
+
expect(layoutService.drawerState.getValue().left).toBeDefined()
|
|
772
|
+
|
|
773
|
+
const removeDrawerSpy = vi.spyOn(layoutService, 'removeDrawer')
|
|
774
|
+
const drawer = document.querySelector('shade-drawer') as HTMLElement
|
|
775
|
+
drawer.remove()
|
|
776
|
+
await flushUpdates()
|
|
777
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
778
|
+
|
|
779
|
+
expect(removeDrawerSpy).toHaveBeenCalledWith('right')
|
|
780
|
+
expect(removeDrawerSpy).not.toHaveBeenCalledWith('left')
|
|
781
|
+
})
|
|
782
|
+
})
|
|
783
|
+
})
|
|
784
|
+
|
|
721
785
|
describe('preserving user interactions', () => {
|
|
722
786
|
it('should not reset drawer state if already initialized', async () => {
|
|
723
787
|
await usingAsync(new Injector(), async (injector) => {
|
|
@@ -143,6 +143,11 @@ export const Drawer = Shade<DrawerProps>({
|
|
|
143
143
|
layoutService.setDrawerWidth(position, width)
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// Clean up drawer state from LayoutService when this component is disposed
|
|
147
|
+
useDisposable('drawer-cleanup', () => ({
|
|
148
|
+
[Symbol.dispose]: () => layoutService.removeDrawer(position),
|
|
149
|
+
}))
|
|
150
|
+
|
|
146
151
|
// Subscribe to drawer state
|
|
147
152
|
const [drawerState] = useObservable('drawerState', layoutService.drawerState)
|
|
148
153
|
const isOpen = drawerState[position]?.open ?? false
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createComponent, Shade } from '@furystack/shades'
|
|
2
2
|
import { cssVariableTheme } from '../services/css-variable-theme.js'
|
|
3
|
-
import { getTextColor } from '../services/get-text-color.js'
|
|
4
3
|
import type { NotyModel } from '../services/noty-service.js'
|
|
5
4
|
import { NotyService } from '../services/noty-service.js'
|
|
6
5
|
import { ThemeProviderService } from '../services/theme-provider-service.js'
|
|
@@ -97,7 +96,6 @@ export const NotyComponent = Shade<{ model: NotyModel; onDismiss: () => void }>(
|
|
|
97
96
|
|
|
98
97
|
const themeProvider = injector.getInstance(ThemeProviderService)
|
|
99
98
|
const colors = themeProvider.theme.palette[props.model.type]
|
|
100
|
-
const textColor = getTextColor(colors.main)
|
|
101
99
|
|
|
102
100
|
const removeSelf = async () => {
|
|
103
101
|
const hostEl = wrapperRef.current?.closest('shade-noty') as HTMLElement | null
|
|
@@ -123,7 +121,7 @@ export const NotyComponent = Shade<{ model: NotyModel; onDismiss: () => void }>(
|
|
|
123
121
|
|
|
124
122
|
useHostProps({
|
|
125
123
|
'data-noty-type': props.model.type,
|
|
126
|
-
style: { '--noty-bg': colors.main, '--noty-text':
|
|
124
|
+
style: { '--noty-bg': colors.main, '--noty-text': colors.mainContrast },
|
|
127
125
|
})
|
|
128
126
|
|
|
129
127
|
return (
|
|
@@ -67,7 +67,7 @@ describe('css-variable-theme', () => {
|
|
|
67
67
|
expect(cssVariableTheme.typography.fontWeight.bold).toBe('var(--shades-theme-typography-font-weight-bold)')
|
|
68
68
|
expect(cssVariableTheme.typography.lineHeight.tight).toBe('var(--shades-theme-typography-line-height-tight)')
|
|
69
69
|
expect(cssVariableTheme.typography.lineHeight.normal).toBe('var(--shades-theme-typography-line-height-normal)')
|
|
70
|
-
expect(cssVariableTheme.typography.textShadow).toBe('var(--shades-theme-typography-text-shadow
|
|
70
|
+
expect(cssVariableTheme.typography.textShadow).toBe('var(--shades-theme-typography-text-shadow)')
|
|
71
71
|
})
|
|
72
72
|
|
|
73
73
|
it('should have transition properties with CSS variable references', () => {
|
|
@@ -446,6 +446,48 @@ describe('css-variable-theme', () => {
|
|
|
446
446
|
expect(root.style.getPropertyValue('--shades-theme-spacing-md')).toBe('16px')
|
|
447
447
|
expect(root.style.getPropertyValue('--shades-theme-spacing-xl')).toBe('32px')
|
|
448
448
|
})
|
|
449
|
+
|
|
450
|
+
it('should set CSS variables on a custom root element instead of :root', () => {
|
|
451
|
+
const customRoot = document.createElement('div')
|
|
452
|
+
document.body.appendChild(customRoot)
|
|
453
|
+
|
|
454
|
+
useThemeCssVariables(
|
|
455
|
+
{
|
|
456
|
+
text: { primary: '#ff0000' },
|
|
457
|
+
divider: '#00ff00',
|
|
458
|
+
},
|
|
459
|
+
customRoot,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
expect(customRoot.style.getPropertyValue('--shades-theme-text-primary')).toBe('#ff0000')
|
|
463
|
+
expect(customRoot.style.getPropertyValue('--shades-theme-divider')).toBe('#00ff00')
|
|
464
|
+
expect(root.style.getPropertyValue('--shades-theme-text-primary')).toBe('')
|
|
465
|
+
|
|
466
|
+
customRoot.remove()
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('should scope nested palette variables to custom root element', () => {
|
|
470
|
+
const customRoot = document.createElement('div')
|
|
471
|
+
document.body.appendChild(customRoot)
|
|
472
|
+
|
|
473
|
+
useThemeCssVariables(
|
|
474
|
+
{
|
|
475
|
+
palette: {
|
|
476
|
+
primary: {
|
|
477
|
+
main: '#1976d2',
|
|
478
|
+
mainContrast: '#ffffff',
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
customRoot,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
expect(customRoot.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('#1976d2')
|
|
486
|
+
expect(customRoot.style.getPropertyValue('--shades-theme-palette-primary-main-contrast')).toBe('#ffffff')
|
|
487
|
+
expect(root.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('')
|
|
488
|
+
|
|
489
|
+
customRoot.remove()
|
|
490
|
+
})
|
|
449
491
|
})
|
|
450
492
|
|
|
451
493
|
describe('buildTransition', () => {
|
|
@@ -17,7 +17,7 @@ export const cssVariableTheme = {
|
|
|
17
17
|
background: {
|
|
18
18
|
default: 'var(--shades-theme-background-default)',
|
|
19
19
|
paper: 'var(--shades-theme-background-paper)',
|
|
20
|
-
paperImage: 'var(--shades-theme-background-paper-image
|
|
20
|
+
paperImage: 'var(--shades-theme-background-paper-image)',
|
|
21
21
|
},
|
|
22
22
|
palette: {
|
|
23
23
|
primary: {
|
|
@@ -87,7 +87,7 @@ export const cssVariableTheme = {
|
|
|
87
87
|
lg: 'var(--shades-theme-shape-border-radius-lg)',
|
|
88
88
|
full: 'var(--shades-theme-shape-border-radius-full)',
|
|
89
89
|
},
|
|
90
|
-
borderWidth: 'var(--shades-theme-shape-border-width
|
|
90
|
+
borderWidth: 'var(--shades-theme-shape-border-width)',
|
|
91
91
|
},
|
|
92
92
|
shadows: {
|
|
93
93
|
none: 'var(--shades-theme-shadows-none)',
|
|
@@ -127,7 +127,7 @@ export const cssVariableTheme = {
|
|
|
127
127
|
wider: 'var(--shades-theme-typography-letter-spacing-wider)',
|
|
128
128
|
widest: 'var(--shades-theme-typography-letter-spacing-widest)',
|
|
129
129
|
},
|
|
130
|
-
textShadow: 'var(--shades-theme-typography-text-shadow
|
|
130
|
+
textShadow: 'var(--shades-theme-typography-text-shadow)',
|
|
131
131
|
},
|
|
132
132
|
transitions: {
|
|
133
133
|
duration: {
|
|
@@ -222,8 +222,8 @@ const assignValue = <T extends object>(
|
|
|
222
222
|
}
|
|
223
223
|
})
|
|
224
224
|
}
|
|
225
|
-
export const useThemeCssVariables = (theme: DeepPartial<Theme
|
|
226
|
-
|
|
225
|
+
export const useThemeCssVariables = (theme: DeepPartial<Theme>, root?: HTMLElement) => {
|
|
226
|
+
root ??= document.querySelector(':root') as HTMLElement
|
|
227
227
|
assignValue(cssVariableTheme, theme, root)
|
|
228
228
|
|
|
229
229
|
if (window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches) {
|
|
@@ -221,6 +221,15 @@ describe('LayoutService', () => {
|
|
|
221
221
|
})
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
+
it('should overwrite existing drawer config', () => {
|
|
225
|
+
using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
|
|
226
|
+
service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
|
|
227
|
+
service.initDrawer('left', { open: false, width: '300px', variant: 'permanent' })
|
|
228
|
+
|
|
229
|
+
expect(service.drawerState.getValue().left).toEqual({ open: false, width: '300px', variant: 'permanent' })
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
224
233
|
it('should initialize drawer with all variant types', () => {
|
|
225
234
|
using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
|
|
226
235
|
service.initDrawer('left', { open: true, width: '240px', variant: 'permanent' })
|
|
@@ -234,6 +243,71 @@ describe('LayoutService', () => {
|
|
|
234
243
|
})
|
|
235
244
|
})
|
|
236
245
|
})
|
|
246
|
+
|
|
247
|
+
describe('removeDrawer', () => {
|
|
248
|
+
it('should remove left drawer state', () => {
|
|
249
|
+
using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
|
|
250
|
+
service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
|
|
251
|
+
|
|
252
|
+
service.removeDrawer('left')
|
|
253
|
+
|
|
254
|
+
expect(service.drawerState.getValue().left).toBeUndefined()
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should remove right drawer state', () => {
|
|
259
|
+
using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
|
|
260
|
+
service.initDrawer('right', { open: true, width: '200px', variant: 'temporary' })
|
|
261
|
+
|
|
262
|
+
service.removeDrawer('right')
|
|
263
|
+
|
|
264
|
+
expect(service.drawerState.getValue().right).toBeUndefined()
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should not affect the other drawer when removing one', () => {
|
|
269
|
+
using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
|
|
270
|
+
service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
|
|
271
|
+
service.initDrawer('right', { open: true, width: '200px', variant: 'temporary' })
|
|
272
|
+
|
|
273
|
+
service.removeDrawer('left')
|
|
274
|
+
|
|
275
|
+
expect(service.drawerState.getValue().left).toBeUndefined()
|
|
276
|
+
expect(service.drawerState.getValue().right).toEqual({ open: true, width: '200px', variant: 'temporary' })
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should be a no-op if the drawer does not exist', () => {
|
|
281
|
+
using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
|
|
282
|
+
const stateBefore = service.drawerState.getValue()
|
|
283
|
+
|
|
284
|
+
service.removeDrawer('left')
|
|
285
|
+
|
|
286
|
+
expect(service.drawerState.getValue()).toEqual(stateBefore)
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should be a no-op if the service is already disposed', () => {
|
|
291
|
+
const service = new LayoutService(mockElement as unknown as HTMLElement)
|
|
292
|
+
service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
|
|
293
|
+
service[Symbol.dispose]()
|
|
294
|
+
|
|
295
|
+
expect(() => service.removeDrawer('left')).not.toThrow()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should reset CSS variables after removing a drawer', () => {
|
|
299
|
+
using(new LayoutService(mockElement as unknown as HTMLElement), (service) => {
|
|
300
|
+
service.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
|
|
301
|
+
mockSetProperty.mockClear()
|
|
302
|
+
|
|
303
|
+
service.removeDrawer('left')
|
|
304
|
+
|
|
305
|
+
expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-width', '0px')
|
|
306
|
+
expect(mockSetProperty).toHaveBeenCalledWith('--layout-drawer-left-configured-width', '0px')
|
|
307
|
+
expect(mockSetProperty).toHaveBeenCalledWith('--layout-content-margin-left', '0px')
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
})
|
|
237
311
|
})
|
|
238
312
|
|
|
239
313
|
describe('CSS Variables', () => {
|
|
@@ -237,6 +237,24 @@ export class LayoutService implements Disposable {
|
|
|
237
237
|
})
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Removes a drawer from the state, clearing its configuration.
|
|
242
|
+
* Use this when a Drawer component is unmounted to prevent stale state
|
|
243
|
+
* from affecting content margins.
|
|
244
|
+
*
|
|
245
|
+
* @param position - Which drawer to remove ('left' or 'right')
|
|
246
|
+
*/
|
|
247
|
+
public removeDrawer(position: 'left' | 'right'): void {
|
|
248
|
+
if (this.drawerState.isDisposed) return
|
|
249
|
+
|
|
250
|
+
const currentState = this.drawerState.getValue()
|
|
251
|
+
|
|
252
|
+
if (currentState[position]) {
|
|
253
|
+
const { [position]: _, ...rest } = currentState
|
|
254
|
+
this.drawerState.setValue(rest)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
240
258
|
/**
|
|
241
259
|
* Sets the top gap spacing between AppBar and content.
|
|
242
260
|
*
|