kopia lustrzana https://github.com/shoelace-style/shoelace
				
				
				
			more finishing touches + tests
							rodzic
							
								
									563ed81984
								
							
						
					
					
						commit
						913243f8c1
					
				|  | @ -25,11 +25,10 @@ export default css` | ||||||
|     color: var(--sl-color-neutral-700); |     color: var(--sl-color-neutral-700); | ||||||
|     padding: var(--sl-spacing-x-small) var(--sl-spacing-medium) var(--sl-spacing-x-small) var(--sl-spacing-x-small); |     padding: var(--sl-spacing-x-small) var(--sl-spacing-medium) var(--sl-spacing-x-small) var(--sl-spacing-x-small); | ||||||
|     transition: var(--sl-transition-fast) fill; |     transition: var(--sl-transition-fast) fill; | ||||||
|     user-select: none; |  | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   :host(:hover) .option:not(.option--current):not(.option--disabled) { |   .option--hover:not(.option--current):not(.option--disabled) { | ||||||
|     background-color: var(--sl-color-neutral-100); |     background-color: var(--sl-color-neutral-100); | ||||||
|     color: var(--sl-color-neutral-1000); |     color: var(--sl-color-neutral-1000); | ||||||
|   } |   } | ||||||
|  | @ -80,4 +79,11 @@ export default css` | ||||||
|   .option__suffix::slotted(*) { |   .option__suffix::slotted(*) { | ||||||
|     margin-inline-start: var(--sl-spacing-x-small); |     margin-inline-start: var(--sl-spacing-x-small); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   @media (forced-colors: active) { | ||||||
|  |     :host(:hover:not([aria-disabled='true'])) .option { | ||||||
|  |       outline: dashed 1px SelectedItem; | ||||||
|  |       outline-offset: -1px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| `;
 | `;
 | ||||||
|  |  | ||||||
|  | @ -6,10 +6,10 @@ describe('<sl-option>', () => { | ||||||
|   it('passes accessibility test', async () => { |   it('passes accessibility test', async () => { | ||||||
|     const el = await fixture<SlOption>(html` |     const el = await fixture<SlOption>(html` | ||||||
|       <sl-select label="Select one"> |       <sl-select label="Select one"> | ||||||
|         <sl-option>Option 1</sl-option> |         <sl-option value="1">Option 1</sl-option> | ||||||
|         <sl-option>Option 2</sl-option> |         <sl-option value="2">Option 2</sl-option> | ||||||
|         <sl-option>Option 3</sl-option> |         <sl-option value="3">Option 3</sl-option> | ||||||
|         <sl-option disabled>Disabled</sl-option> |         <sl-option value="4" disabled>Disabled</sl-option> | ||||||
|       </sl-select> |       </sl-select> | ||||||
|     `);
 |     `);
 | ||||||
|     await expect(el).to.be.accessible(); |     await expect(el).to.be.accessible(); | ||||||
|  |  | ||||||
|  | @ -36,6 +36,7 @@ export default class SlOption extends ShoelaceElement { | ||||||
| 
 | 
 | ||||||
|   @state() current = false; // the user has keyed into the option, but hasn't selected it yet (shows a highlight)
 |   @state() current = false; // the user has keyed into the option, but hasn't selected it yet (shows a highlight)
 | ||||||
|   @state() selected = false; // the option is selected and has aria-selected="true"
 |   @state() selected = false; // the option is selected and has aria-selected="true"
 | ||||||
|  |   @state() hasHover = false; // we need this because Safari doesn't honor :hover styles while dragging
 | ||||||
| 
 | 
 | ||||||
|   @query('.option__label') defaultSlot: HTMLSlotElement; |   @query('.option__label') defaultSlot: HTMLSlotElement; | ||||||
| 
 | 
 | ||||||
|  | @ -94,6 +95,14 @@ export default class SlOption extends ShoelaceElement { | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   handleMouseEnter() { | ||||||
|  |     this.hasHover = true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   handleMouseLeave() { | ||||||
|  |     this.hasHover = false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   render() { |   render() { | ||||||
|     return html` |     return html` | ||||||
|       <div |       <div | ||||||
|  | @ -102,8 +111,11 @@ export default class SlOption extends ShoelaceElement { | ||||||
|           option: true, |           option: true, | ||||||
|           'option--current': this.current, |           'option--current': this.current, | ||||||
|           'option--disabled': this.disabled, |           'option--disabled': this.disabled, | ||||||
|           'option--selected': this.selected |           'option--selected': this.selected, | ||||||
|  |           'option--hover': this.hasHover | ||||||
|         })} |         })} | ||||||
|  |         @mouseenter=${this.handleMouseEnter} | ||||||
|  |         @mouseleave=${this.handleMouseLeave} | ||||||
|       > |       > | ||||||
|         <sl-icon part="checked-icon" class="option__check" name="check" library="system" aria-hidden="true"></sl-icon> |         <sl-icon part="checked-icon" class="option__check" name="check" library="system" aria-hidden="true"></sl-icon> | ||||||
|         <slot part="prefix" name="prefix" class="option__prefix"></slot> |         <slot part="prefix" name="prefix" class="option__prefix"></slot> | ||||||
|  |  | ||||||
|  | @ -39,8 +39,7 @@ describe('<sl-select>', () => { | ||||||
|     `);
 |     `);
 | ||||||
|     const disabledOption = el.querySelector('sl-option[disabled]')!; |     const disabledOption = el.querySelector('sl-option[disabled]')!; | ||||||
| 
 | 
 | ||||||
|     await clickOnElement(el); |     await el.show(); | ||||||
|     await waitForEvent(el, 'sl-after-show'); |  | ||||||
|     await clickOnElement(disabledOption); |     await clickOnElement(disabledOption); | ||||||
|     await el.updateComplete; |     await el.updateComplete; | ||||||
| 
 | 
 | ||||||
|  | @ -81,8 +80,7 @@ describe('<sl-select>', () => { | ||||||
|       el.addEventListener('sl-change', changeHandler); |       el.addEventListener('sl-change', changeHandler); | ||||||
|       el.addEventListener('sl-input', inputHandler); |       el.addEventListener('sl-input', inputHandler); | ||||||
| 
 | 
 | ||||||
|       await clickOnElement(el); |       await el.show(); | ||||||
|       await waitForEvent(el, 'sl-after-show'); |  | ||||||
|       await clickOnElement(secondOption); |       await clickOnElement(secondOption); | ||||||
|       await el.updateComplete; |       await el.updateComplete; | ||||||
| 
 | 
 | ||||||
|  | @ -293,9 +291,7 @@ describe('<sl-select>', () => { | ||||||
|       const select = form.querySelector('sl-select')!; |       const select = form.querySelector('sl-select')!; | ||||||
|       const option2 = form.querySelectorAll('sl-option')![1]; |       const option2 = form.querySelectorAll('sl-option')![1]; | ||||||
| 
 | 
 | ||||||
|       await clickOnElement(select); |       await select.show(); | ||||||
|       await waitForEvent(select, 'sl-after-show'); |  | ||||||
| 
 |  | ||||||
|       await clickOnElement(option2); |       await clickOnElement(option2); | ||||||
|       await select.updateComplete; |       await select.updateComplete; | ||||||
|       expect(select.value).to.equal('option-2'); |       expect(select.value).to.equal('option-2'); | ||||||
|  | @ -326,4 +322,73 @@ describe('<sl-select>', () => { | ||||||
| 
 | 
 | ||||||
|     expect(displayInput.value).to.equal('updated'); |     expect(displayInput.value).to.equal('updated'); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   it('should emit sl-focus and sl-blur when receiving and losing focus', async () => { | ||||||
|  |     const el = await fixture<SlSelect>(html` | ||||||
|  |       <sl-select value="option-1"> | ||||||
|  |         <sl-option value="option-1">Option 1</sl-option> | ||||||
|  |         <sl-option value="option-2">Option 2</sl-option> | ||||||
|  |         <sl-option value="option-3">Option 3</sl-option> | ||||||
|  |       </sl-select> | ||||||
|  |     `);
 | ||||||
|  |     const focusHandler = sinon.spy(); | ||||||
|  |     const blurHandler = sinon.spy(); | ||||||
|  | 
 | ||||||
|  |     el.addEventListener('sl-focus', focusHandler); | ||||||
|  |     el.addEventListener('sl-blur', blurHandler); | ||||||
|  | 
 | ||||||
|  |     el.focus(); | ||||||
|  |     await el.updateComplete; | ||||||
|  |     el.blur(); | ||||||
|  |     await el.updateComplete; | ||||||
|  | 
 | ||||||
|  |     expect(focusHandler).to.have.been.calledOnce; | ||||||
|  |     expect(blurHandler).to.have.been.calledOnce; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should emit sl-clear when the clear button is clicked', async () => { | ||||||
|  |     const el = await fixture<SlSelect>(html` | ||||||
|  |       <sl-select value="option-1" clearable> | ||||||
|  |         <sl-option value="option-1">Option 1</sl-option> | ||||||
|  |         <sl-option value="option-2">Option 2</sl-option> | ||||||
|  |         <sl-option value="option-3">Option 3</sl-option> | ||||||
|  |       </sl-select> | ||||||
|  |     `);
 | ||||||
|  |     const clearHandler = sinon.spy(); | ||||||
|  |     const clearButton = el.shadowRoot!.querySelector('[part~="clear-button"]')!; | ||||||
|  | 
 | ||||||
|  |     el.addEventListener('sl-clear', clearHandler); | ||||||
|  |     await el.show(); | ||||||
|  |     await clickOnElement(clearButton); | ||||||
|  |     await el.updateComplete; | ||||||
|  | 
 | ||||||
|  |     expect(clearHandler).to.have.been.calledOnce; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should emit sl-show, sl-after-show, sl-hide, and sl-after-hide events when the listbox opens and closes', async () => { | ||||||
|  |     const el = await fixture<SlSelect>(html` | ||||||
|  |       <sl-select value="option-1"> | ||||||
|  |         <sl-option value="option-1">Option 1</sl-option> | ||||||
|  |         <sl-option value="option-2">Option 2</sl-option> | ||||||
|  |         <sl-option value="option-3">Option 3</sl-option> | ||||||
|  |       </sl-select> | ||||||
|  |     `);
 | ||||||
|  |     const showHandler = sinon.spy(); | ||||||
|  |     const afterShowHandler = sinon.spy(); | ||||||
|  |     const hideHandler = sinon.spy(); | ||||||
|  |     const afterHideHandler = sinon.spy(); | ||||||
|  | 
 | ||||||
|  |     el.addEventListener('sl-show', showHandler); | ||||||
|  |     el.addEventListener('sl-after-show', afterShowHandler); | ||||||
|  |     el.addEventListener('sl-hide', hideHandler); | ||||||
|  |     el.addEventListener('sl-after-hide', afterHideHandler); | ||||||
|  | 
 | ||||||
|  |     await el.show(); | ||||||
|  |     expect(showHandler).to.have.been.calledOnce; | ||||||
|  |     expect(afterShowHandler).to.have.been.calledOnce; | ||||||
|  | 
 | ||||||
|  |     await el.hide(); | ||||||
|  |     expect(hideHandler).to.have.been.calledOnce; | ||||||
|  |     expect(afterHideHandler).to.have.been.calledOnce; | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -358,7 +358,6 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon | ||||||
|     this.displayInput.focus(); |     this.displayInput.focus(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // We use mousedown/mouseup instead of click to allow macOS-style menu behavior
 |  | ||||||
|   private handleComboboxMouseDown(event: MouseEvent) { |   private handleComboboxMouseDown(event: MouseEvent) { | ||||||
|     const path = event.composedPath(); |     const path = event.composedPath(); | ||||||
|     const isIconButton = path.some(el => el instanceof Element && el.tagName.toLowerCase() === 'sl-icon-button'); |     const isIconButton = path.some(el => el instanceof Element && el.tagName.toLowerCase() === 'sl-icon-button'); | ||||||
|  | @ -396,8 +395,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon | ||||||
|     event.preventDefault(); |     event.preventDefault(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // We use mousedown/mouseup instead of click to allow macOS-style menu behavior
 |   private handleOptionClick(event: MouseEvent) { | ||||||
|   private handleOptionMouseUp(event: MouseEvent) { |  | ||||||
|     const target = event.target as HTMLElement; |     const target = event.target as HTMLElement; | ||||||
|     const option = target.closest('sl-option'); |     const option = target.closest('sl-option'); | ||||||
|     const oldValue = this.value; |     const oldValue = this.value; | ||||||
|  | @ -433,7 +431,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon | ||||||
|     allOptions.forEach(option => { |     allOptions.forEach(option => { | ||||||
|       if (values.includes(option.value)) { |       if (values.includes(option.value)) { | ||||||
|         console.error( |         console.error( | ||||||
|           `An option with duplicate values has been found in <sl-select>. All options must be unique.`, |           `An option with duplicate values has been found in <sl-select>. All options must have unique values.`, | ||||||
|           option |           option | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  | @ -766,7 +764,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon | ||||||
|               part="listbox" |               part="listbox" | ||||||
|               class="select__listbox" |               class="select__listbox" | ||||||
|               tabindex="-1" |               tabindex="-1" | ||||||
|               @mouseup=${this.handleOptionMouseUp} |               @mouseup=${this.handleOptionClick} | ||||||
|               @slotchange=${this.handleDefaultSlotChange} |               @slotchange=${this.handleDefaultSlotChange} | ||||||
|             ></slot> |             ></slot> | ||||||
|           </sl-popup> |           </sl-popup> | ||||||
|  |  | ||||||
		Ładowanie…
	
		Reference in New Issue
	
	 Cory LaViska
						Cory LaViska