kopia lustrzana https://github.com/shoelace-style/shoelace
keep on keepin on
rodzic
46fda5f0a6
commit
1f457cdde0
|
@ -16,27 +16,29 @@ export default css`
|
|||
.option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
font-family: var(--sl-font-sans);
|
||||
font-size: var(--sl-font-size-medium);
|
||||
font-weight: var(--sl-font-weight-normal);
|
||||
line-height: var(--sl-line-height-normal);
|
||||
letter-spacing: var(--sl-letter-spacing-normal);
|
||||
color: var(--sl-color-neutral-700);
|
||||
padding: var(--sl-spacing-2x-small) var(--sl-spacing-2x-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;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host(:hover) .option:not(.option--current) {
|
||||
:host(:hover) .option:not(.option--current):not(.option--disabled) {
|
||||
background-color: var(--sl-color-neutral-100);
|
||||
color: var(--sl-color-neutral-1000);
|
||||
}
|
||||
|
||||
.option--current {
|
||||
.option--current,
|
||||
.option--current.option--disabled {
|
||||
background-color: var(--sl-color-primary-600);
|
||||
color: var(--sl-color-neutral-0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.option--disabled {
|
||||
|
@ -48,7 +50,7 @@ export default css`
|
|||
.option__label {
|
||||
flex: 1 1 auto;
|
||||
display: inline-block;
|
||||
padding: 0 var(--sl-spacing-2x-small);
|
||||
line-height: var(--sl-line-height-dense);
|
||||
}
|
||||
|
||||
.option .option__check {
|
||||
|
@ -56,15 +58,16 @@ export default css`
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5em;
|
||||
visibility: hidden;
|
||||
padding-inline-end: var(--sl-spacing-2x-small);
|
||||
}
|
||||
|
||||
.option--selected .option__check {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.option__prefix {
|
||||
.option__prefix,
|
||||
.option__suffix {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -74,12 +77,6 @@ export default css`
|
|||
margin-inline-end: var(--sl-spacing-x-small);
|
||||
}
|
||||
|
||||
.option__suffix {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.option__suffix::slotted(*) {
|
||||
margin-inline-start: var(--sl-spacing-x-small);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,43 @@
|
|||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { expect, fixture, html, waitUntil, aTimeout } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type SlOption from './option';
|
||||
|
||||
describe('<sl-option>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-option></sl-option> `);
|
||||
it('passes accessibility test', async () => {
|
||||
const el = await fixture<SlOption>(html`
|
||||
<sl-select label="Select one">
|
||||
<sl-option>Option 1</sl-option>
|
||||
<sl-option>Option 2</sl-option>
|
||||
<sl-option>Option 3</sl-option>
|
||||
<sl-option disabled>Disabled</sl-option>
|
||||
</sl-select>
|
||||
`);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
expect(el).to.exist;
|
||||
it('default properties', async () => {
|
||||
const el = await fixture<SlOption>(html` <sl-option>Test</sl-option> `);
|
||||
|
||||
expect(el.value).to.equal('');
|
||||
expect(el.disabled).to.be.false;
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('false');
|
||||
});
|
||||
|
||||
it('changes aria attributes', async () => {
|
||||
const el = await fixture<SlOption>(html` <sl-option>Test</sl-option> `);
|
||||
|
||||
el.disabled = true;
|
||||
await aTimeout(100);
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||
});
|
||||
|
||||
it('emit sl-label-change when the label changes', async () => {
|
||||
const el = await fixture<SlOption>(html` <sl-option>Test</sl-option> `);
|
||||
|
||||
const labelChangeHandler = sinon.spy();
|
||||
el.textContent = 'New Text';
|
||||
el.addEventListener('sl-label-change', labelChangeHandler);
|
||||
await waitUntil(() => labelChangeHandler.calledOnce);
|
||||
expect(labelChangeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,28 +10,33 @@ import styles from './option.styles';
|
|||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Short summary of the component's intended use.
|
||||
* @summary Options define the selectable items within various form controls such as [select](/components/select).
|
||||
*
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
* @status stable
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @event sl-label-change - Emitted when the option's label changes. For performance reasons, this event is only emitted
|
||||
* when the default slot's `slotchange` event is triggered. It will not fire when the label is first set.
|
||||
* when the default slot's `slotchange` event is triggered. It will not fire when the label is first set. Useful for
|
||||
* parent controls that want to observe label changes without attaching an expensive mutation observer.
|
||||
*
|
||||
* @slot - The default slot.
|
||||
* @slot example - An example slot.
|
||||
* @slot - The option's label.
|
||||
* @slot prefix - Used to prepend an icon or similar element to the menu item.
|
||||
* @slot suffix - Used to append an icon or similar element to the menu item.
|
||||
*
|
||||
* @csspart checked-icon - The checked icon, an `<sl-icon>` element.
|
||||
* @csspart base - The component's base wrapper.
|
||||
*
|
||||
* @cssproperty --example - An example CSS custom property.
|
||||
* @csspart label - The option's label.
|
||||
* @csspart prefix - The container that wraps the prefix.
|
||||
* @csspart suffix - The container that wraps the suffix.
|
||||
*/
|
||||
@customElement('sl-option')
|
||||
export default class SlOption extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private cachedTextLabel: string;
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('.option__label') defaultSlot: HTMLSlotElement;
|
||||
|
@ -82,18 +87,18 @@ export default class SlOption extends ShoelaceElement {
|
|||
render() {
|
||||
return html`
|
||||
<div
|
||||
part="base"
|
||||
class=${classMap({
|
||||
option: true,
|
||||
'option--current': this.current,
|
||||
'option--disabled': this.disabled,
|
||||
'option--selected': this.selected
|
||||
})}
|
||||
>
|
||||
<span part="checked-icon" class="option__check">
|
||||
<sl-icon name="check" library="system" aria-hidden="true"></sl-icon>
|
||||
</span>
|
||||
<slot name="prefix" class="option__prefix"></slot>
|
||||
<slot class="option__label" @slotchange=${this.handleDefaultSlotChange}></slot>
|
||||
<slot name="suffix" class="option__suffix"></slot>
|
||||
<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="label" class="option__label" @slotchange=${this.handleDefaultSlotChange}></slot>
|
||||
<slot part="suffix" name="suffix" class="option__suffix"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,23 @@ describe('<sl-select>', () => {
|
|||
expect(el.displayInput.disabled).to.be.true;
|
||||
});
|
||||
|
||||
it('should not allow selection when the option is disabled', 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" disabled>Option 2</sl-option>
|
||||
</sl-select>
|
||||
`);
|
||||
const disabledOption = el.querySelector('sl-option[disabled]')!;
|
||||
|
||||
await clickOnElement(el);
|
||||
await waitForEvent(el, 'sl-after-show');
|
||||
await clickOnElement(disabledOption);
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.value).to.equal('option-1');
|
||||
});
|
||||
|
||||
it('should focus the select when clicking on the label', async () => {
|
||||
const el = await fixture<SlSelect>(html`
|
||||
<sl-select label="Select One">
|
||||
|
|
|
@ -199,7 +199,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||
const currentOption = this.getCurrentOption();
|
||||
if (currentOption && !currentOption.disabled) {
|
||||
this.setSelectedOption(currentOption);
|
||||
this.displayLabel = currentOption.textContent ?? '';
|
||||
this.displayLabel = (currentOption.textContent ?? '').trim();
|
||||
this.value = currentOption.value;
|
||||
this.valueInput.value = currentOption.value; // synchronous update for validation
|
||||
this.invalid = !this.checkValidity();
|
||||
|
@ -372,7 +372,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||
if (option) {
|
||||
this.setSelectedOption(option);
|
||||
this.value = option.value;
|
||||
this.displayLabel = option.textContent ?? '';
|
||||
this.displayLabel = (option.textContent ?? '').trim();
|
||||
} else {
|
||||
// Clear selection
|
||||
this.setSelectedOption(null);
|
||||
|
@ -446,7 +446,9 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||
|
||||
if (option) {
|
||||
this.value = option.value;
|
||||
this.displayLabel = option.textContent ?? '';
|
||||
this.valueInput.value = option.value; // synchronous update for validation
|
||||
this.invalid = !this.checkValidity();
|
||||
this.displayLabel = (option.textContent ?? '').trim();
|
||||
} else {
|
||||
// No option, reset the control
|
||||
this.value = '';
|
||||
|
|
Ładowanie…
Reference in New Issue