|
1 | 1 | <script lang="ts"> |
2 | | - import { paletteStore } from '../store/PaletteStore'; |
3 | 2 | import { onDestroy, onMount, afterUpdate } from 'svelte'; |
4 | | - // import tinyKeys, { parseKeybinding } from 'tinykeys'; |
| 3 | + import { setContext as setThemeContext } from 'svelte'; |
| 4 | + import { paletteStore } from '../store/PaletteStore'; |
5 | 5 | import Portal from './Portal.svelte'; |
6 | 6 | import ResultPanel from './ResultPanel.svelte'; |
7 | 7 | import KeyboardButton from './KeyboardButton.svelte'; |
8 | 8 | import createShortcuts from '../utils/createShortcuts'; |
9 | | - import { createFuse, formatResults, getNonEmptyArray, runAction } from '../utils'; |
| 9 | + import { createFuse, formatResults, getNonEmptyArray, runAction, toCssString } from '../utils'; |
10 | 10 | import createStoreMethods from '../utils/createStoreMethods'; |
11 | 11 | import createActionMap from '../utils/createActionMap'; |
12 | | - import type { ActionId, commands, storeParams } from '$lib/types'; |
| 12 | + import { THEME_CONTEXT } from '../constants'; |
| 13 | + import type { ActionId, commands, storeParams, className, cssStyle } from '$lib/types'; |
| 14 | + import type { Properties } from 'csstype'; |
13 | 15 |
|
14 | 16 | export let commands: commands = []; |
| 17 | + export let placeholder: string = 'Search for an action'; |
| 18 | +
|
| 19 | + // style classes |
| 20 | +
|
| 21 | + export let inputClass: className = null; |
| 22 | + export let overlayClass: className = null; |
| 23 | + export let paletteWrapperInnerClass: className = null; |
| 24 | + export let resultsContainerClass: className = null; |
| 25 | + export let resultContainerClass: className = null; |
| 26 | + export let optionSelectedClass: className = null; |
| 27 | + export let titleClass: className = null; |
| 28 | + export let subtitleClass: className = null; |
| 29 | + export let descriptionClass: className = null; |
| 30 | + export let keyboardButtonClass: className = null; |
| 31 | + export let unstyled = false; |
| 32 | +
|
| 33 | + // style props (convert to css) |
| 34 | +
|
| 35 | + export let inputStyle: Properties = {}; |
| 36 | + export let overlayStyle: Properties = {}; |
| 37 | + export let paletteWrapperInnerStyle: Properties = {}; |
| 38 | + export let resultsContainerStyle: Properties = {}; |
| 39 | + export let resultContainerStyle: Properties = {}; |
| 40 | + export let optionSelectedStyle: Properties = {}; |
| 41 | + export let titleStyle: Properties = {}; |
| 42 | + export let subtitleStyle: Properties = {}; |
| 43 | + export let descriptionStyle: Properties = {}; |
| 44 | + export let keyboardButtonStyle: Properties = {}; |
| 45 | +
|
15 | 46 | let wrapperElement: HTMLDivElement; |
16 | 47 | let searchInputRef: HTMLInputElement; |
17 | 48 | let commandPaletteRef: HTMLElement; |
|
25 | 56 |
|
26 | 57 | let actions: commands = []; |
27 | 58 |
|
| 59 | + // set themes to context to pass down to deeply nested components |
| 60 | +
|
| 61 | + setThemeContext(THEME_CONTEXT, { |
| 62 | + inputClass, |
| 63 | + overlayClass, |
| 64 | + paletteWrapperInnerClass, |
| 65 | + resultsContainerClass, |
| 66 | + resultContainerClass, |
| 67 | + optionSelectedClass, |
| 68 | + titleClass, |
| 69 | + subtitleClass, |
| 70 | + descriptionClass, |
| 71 | + keyboardButtonClass, |
| 72 | + unstyled, |
| 73 | + inputStyle: toCssString(inputStyle), |
| 74 | + overlayStyle: toCssString(overlayStyle), |
| 75 | + paletteWrapperInnerStyle: toCssString(paletteWrapperInnerStyle), |
| 76 | + resultsContainerStyle: toCssString(resultsContainerStyle), |
| 77 | + resultContainerStyle: toCssString(resultContainerStyle), |
| 78 | + optionSelectedStyle: toCssString(optionSelectedStyle), |
| 79 | + titleStyle: toCssString(titleStyle), |
| 80 | + subtitleStyle: toCssString(subtitleStyle), |
| 81 | + descriptionStyle: toCssString(descriptionStyle), |
| 82 | + keyboardButtonStyle: toCssString(keyboardButtonStyle) |
| 83 | + }); |
| 84 | +
|
28 | 85 | const storeMethods = createStoreMethods(); |
29 | 86 | const actionMap = createActionMap(commands); |
30 | 87 | let formattedEscKey = ''; |
|
54 | 111 | lastActiveElement?.focus(); |
55 | 112 | }; |
56 | 113 |
|
57 | | - const closePalette = () => { |
| 114 | + const closePalette = (event?: KeyboardEvent) => { |
| 115 | + event?.preventDefault?.(); |
58 | 116 | closeCommandPalette(); |
59 | 117 | focusLastElement(); |
60 | 118 | }; |
|
170 | 228 |
|
171 | 229 | <Portal target="body"> |
172 | 230 | {#if isPaletteVisible} |
173 | | - <div id="wrapper" bind:this={wrapperElement}> |
| 231 | + <div |
| 232 | + id="wrapper" |
| 233 | + class={overlayClass} |
| 234 | + style={toCssString(overlayStyle)} |
| 235 | + class:wrapper={!unstyled} |
| 236 | + bind:this={wrapperElement} |
| 237 | + > |
174 | 238 | <div |
175 | 239 | class="paletteWrapper" |
176 | 240 | role="combobox" |
177 | 241 | aria-expanded={true} |
178 | 242 | aria-haspopup="listbox" |
179 | 243 | aria-controls={'uniqId'} |
180 | 244 | > |
181 | | - <div class="paletteWrapperInner" bind:this={commandPaletteRef}> |
| 245 | + <div |
| 246 | + class:paletteWrapperInner={!unstyled} |
| 247 | + class={paletteWrapperInnerClass} |
| 248 | + style={toCssString(paletteWrapperInnerStyle)} |
| 249 | + bind:this={commandPaletteRef} |
| 250 | + > |
182 | 251 | <form autocomplete="off" role="search" novalidate on:submit={(ev) => ev.preventDefault()}> |
183 | 252 | <label for={searchInputId}>Search for an action</label> |
184 | 253 | <input |
185 | 254 | type="search" |
186 | | - placeholder="Search for an action" |
| 255 | + class={inputClass} |
| 256 | + class:paletteInput={!unstyled} |
| 257 | + style={toCssString(inputStyle)} |
| 258 | + {placeholder} |
187 | 259 | aria-autocomplete="list" |
188 | 260 | spellcheck={false} |
189 | 261 | aria-activedescendant={`palette-${activeCommand}`} |
|
195 | 267 | on:input={handleSearch} |
196 | 268 | /> |
197 | 269 | <div class="shortcut"> |
198 | | - <KeyboardButton on:KeyboardButtonClicked={closePalette} |
| 270 | + <KeyboardButton on:KeyboardButtonClicked={() => closePalette()} |
199 | 271 | >{formattedEscKey}</KeyboardButton |
200 | 272 | > |
201 | 273 | </div> |
|
208 | 280 | </Portal> |
209 | 281 |
|
210 | 282 | <style> |
211 | | - #wrapper { |
| 283 | + .wrapper { |
212 | 284 | position: fixed; |
213 | 285 | top: 0; |
214 | 286 | left: 0; |
|
221 | 293 | } |
222 | 294 |
|
223 | 295 | :global(.paletteWrapper *) { |
224 | | - padding: 0; |
225 | | - margin: 0; |
226 | 296 | box-sizing: border-box; |
227 | 297 | } |
228 | 298 |
|
|
247 | 317 | width: 100%; |
248 | 318 | } |
249 | 319 |
|
250 | | - input { |
| 320 | + .paletteInput { |
251 | 321 | width: 100%; |
252 | 322 | padding: 1rem; |
253 | 323 | appearance: none; |
254 | 324 | border: none; |
255 | 325 | } |
256 | 326 |
|
257 | | - input::placeholder { |
| 327 | + .paletteInput::placeholder { |
258 | 328 | font-size: 20px; |
259 | 329 | } |
260 | 330 |
|
261 | | - input[type='search']::-webkit-search-decoration, |
262 | | - input[type='search']::-webkit-search-cancel-button, |
263 | | - input[type='search']::-webkit-search-results-button, |
264 | | - input[type='search']::-webkit-search-results-decoration { |
| 331 | + .paletteInput[type='search']::-webkit-search-decoration, |
| 332 | + .paletteInput[type='search']::-webkit-search-cancel-button, |
| 333 | + .paletteInput[type='search']::-webkit-search-results-button, |
| 334 | + .paletteInput[type='search']::-webkit-search-results-decoration { |
265 | 335 | -webkit-appearance: none; |
266 | 336 | } |
267 | | - input:focus { |
| 337 | + .paletteInput:focus { |
268 | 338 | outline: none; |
269 | 339 | } |
270 | 340 |
|
|
286 | 356 | height: 100vh; |
287 | 357 | max-height: 100vh; |
288 | 358 | } |
289 | | - #wrapper { |
| 359 | + .wrapper { |
290 | 360 | padding: 0; |
291 | 361 | } |
292 | 362 | } |
|
0 commit comments