Merge branch 'next' of https://github.com/shoelace-style/shoelace into konnorrogers/update-react-wrappers

This commit is contained in:
konnorrogers
2023-08-18 12:44:15 -04:00
13 changed files with 136 additions and 76 deletions

View File

@@ -7,6 +7,7 @@ docs/search.json
src/components/icon/icons
src/react/index.ts
node_modules
package.json
package-lock.json
tsconfig.json
cdn

View File

@@ -160,6 +160,7 @@
"unbundles",
"unbundling",
"unicons",
"unsanitized",
"unsupportive",
"valpha",
"valuenow",

View File

@@ -454,3 +454,53 @@ const App = () => (
</>
);
```
### Custom Tags
When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your function can return a string of HTML, a <a href="https://lit.dev/docs/templates/overview/">Lit Template</a>, or an [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for each option. The first argument is an `<sl-option>` element and the second argument is the tag's index (its position in the tag list).
Remember that custom tags are rendered in a shadow root. To style them, you can use the `style` attribute in your template or you can add your own [parts](/getting-started/customizing/#css-parts) and target them with the [`::part()`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) selector.
```html:preview
<sl-select
placeholder="Select one"
value="email phone"
multiple
clearable
class="custom-tag"
>
<sl-option value="email">
<sl-icon slot="prefix" name="envelope"></sl-icon>
Email
</sl-option>
<sl-option value="phone">
<sl-icon slot="prefix" name="telephone"></sl-icon>
Phone
</sl-option>
<sl-option value="chat">
<sl-icon slot="prefix" name="chat-dots"></sl-icon>
Chat
</sl-option>
</sl-select>
<script type="module">
const select = document.querySelector('.custom-tag');
select.getTag = (option, index) => {
// Use the same icon used in the <sl-option>
const name = option.querySelector('sl-icon[slot="prefix"]').name;
// You can return a string, a Lit Template, or an HTMLElement here
return `
<sl-tag removable>
<sl-icon name="${name}" style="padding-inline-end: .5rem;"></sl-icon>
${option.getTextLabel()}
</sl-tag>
`;
};
</script>
```
:::warning
Be sure you trust the content you are outputting! Passing unsanitized user input to `getTag()` can result in XSS vulnerabilities.
:::

View File

@@ -462,7 +462,7 @@ To disable the browser's error messages, you need to cancel the `sl-invalid` eve
<sl-button type="reset" variant="default">Reset</sl-button>
</form>
<script>
<script type="module">
const form = document.querySelector('.inline-validation');
const nameError = document.querySelector('#name-error');

View File

@@ -187,7 +187,7 @@ import '@shoelace-style/shoelace/%NPMDIR%/components/rating/rating.js';
import { setBasePath } from '@shoelace-style/shoelace/%NPMDIR%/utilities/base-path.js';
// Set the base path to the folder you copied Shoelace's assets to
setBasePath('/path/to/shoelace/%NPMDIR%
setBasePath('/path/to/shoelace/%NPMDIR%');
// <sl-button>, <sl-icon>, <sl-input>, and <sl-rating> are ready to use!
```

View File

@@ -12,6 +12,11 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> bad
New versions of Shoelace are released as-needed and generally occur when a critical mass of changes have accumulated. At any time, you can see what's coming in the next release by visiting [next.shoelace.style](https://next.shoelace.style).
## Next
- Fixed a regression that caused `<sl-radio-button>` to render incorrectly with gaps [#1523]
- Improved expand/collapse behavior of `<sl-tree>` to work more like users expect [#1521]
## 2.7.0
- Added the experimental `<sl-copy-button>` component [#1473]

View File

@@ -25,15 +25,8 @@
"./dist/react/*": "./dist/react/*",
"./dist/translations/*": "./dist/translations/*"
},
"files": [
"dist",
"cdn"
],
"keywords": [
"web components",
"custom elements",
"components"
],
"files": ["dist", "cdn"],
"keywords": ["web components", "custom elements", "components"],
"repository": {
"type": "git",
"url": "git+https://github.com/shoelace-style/shoelace.git"
@@ -140,9 +133,6 @@
"user-agent-data-types": "^0.3.0"
},
"lint-staged": {
"*.{ts,js}": [
"eslint --max-warnings 0 --cache --fix",
"prettier --write"
]
"*.{ts,js}": ["eslint --max-warnings 0 --cache --fix", "prettier --write"]
}
}

View File

@@ -54,7 +54,7 @@ export default class SlButtonGroup extends ShoelaceElement {
const index = slottedElements.indexOf(el);
const button = findButton(el);
if (button !== null) {
if (button) {
button.classList.add('sl-button-group__button');
button.classList.toggle('sl-button-group__button--first', index === 0);
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);

View File

@@ -20,4 +20,26 @@ describe('<sl-radio-button>', () => {
expect(radio1.checked).to.be.true;
expect(radio2.checked).to.be.false;
});
it('should receive positional classes from <sl-button-group>', async () => {
const radioGroup = await fixture<SlRadioGroup>(html`
<sl-radio-group value="1">
<sl-radio-button id="radio-1" value="1"></sl-radio-button>
<sl-radio-button id="radio-2" value="2"></sl-radio-button>
<sl-radio-button id="radio-3" value="3"></sl-radio-button>
</sl-radio-group>
`);
const radio1 = radioGroup.querySelector<SlRadioButton>('#radio-1')!;
const radio2 = radioGroup.querySelector<SlRadioButton>('#radio-2')!;
const radio3 = radioGroup.querySelector<SlRadioButton>('#radio-3')!;
await Promise.all([radioGroup.updateComplete, radio1.updateComplete, radio2.updateComplete, radio3.updateComplete]);
expect(radio1.classList.contains('sl-button-group__button')).to.be.true;
expect(radio1.classList.contains('sl-button-group__button--first')).to.be.true;
expect(radio2.classList.contains('sl-button-group__button')).to.be.true;
expect(radio2.classList.contains('sl-button-group__button--inner')).to.be.true;
expect(radio3.classList.contains('sl-button-group__button')).to.be.true;
expect(radio3.classList.contains('sl-button-group__button--last')).to.be.true;
});
});

View File

@@ -327,11 +327,8 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
const hasHelpTextSlot = this.hasSlotController.test('help-text');
const hasLabel = this.label ? true : !!hasLabelSlot;
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
const defaultSlot = html`
<span @click=${this.handleRadioClick} @keydown=${this.handleKeyDown} role="presentation">
<slot @slotchange=${this.syncRadios}></slot>
</span>
<slot @slotchange=${this.syncRadios} @click=${this.handleRadioClick} @keydown=${this.handleKeyDown}></slot>
`;
return html`
@@ -378,7 +375,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
${this.hasButtonGroup
? html`
<sl-button-group part="button-group" exportparts="base:button-group__base">
<sl-button-group part="button-group" exportparts="base:button-group__base" role="presentation">
${defaultSlot}
</sl-button-group>
`
@@ -395,6 +392,5 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
</div>
</fieldset>
`;
/* eslint-enable lit-a11y/click-events-have-key-events */
}
}

View File

@@ -8,6 +8,7 @@ import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { property, query, state } from 'lit/decorators.js';
import { scrollIntoView } from '../../internal/scroll.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
@@ -15,7 +16,7 @@ import SlIcon from '../icon/icon.component.js';
import SlPopup from '../popup/popup.component.js';
import SlTag from '../tag/tag.component.js';
import styles from './select.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, TemplateResult } from 'lit';
import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
import type SlOption from '../option/option.component.js';
import type SlRemoveEvent from '../../events/sl-remove.js';
@@ -172,6 +173,31 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
/** The select's required attribute. */
@property({ type: Boolean, reflect: true }) required = false;
/**
* A function that customizes the tags to be rendered when multiple=true. The first argument is the option, the second
* is the current tag's index. The function should return either a Lit TemplateResult or a string containing trusted HTML of the symbol to render at
* the specified value.
*/
@property() getTag: (option: SlOption, index: number) => TemplateResult | string | HTMLElement = option => {
return html`
<sl-tag
part="tag"
exportparts="
base:tag__base,
content:tag__content,
remove-button:tag__remove-button,
remove-button__base:tag__remove-button__base
"
?pill=${this.pill}
size=${this.size}
removable
@sl-remove=${(event: SlRemoveEvent) => this.handleTagRemove(event, option)}
>
${option.getTextLabel()}
</sl-tag>
`;
};
/** Gets the validity state object */
get validity() {
return this.valueInput.validity;
@@ -547,6 +573,21 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
this.formControlController.updateValidity();
});
}
protected get tags() {
return this.selectedOptions.map((option, index) => {
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
const tag = this.getTag(option, index);
// Wrap so we can handle the remove
return html`<div @sl-remove=${(e: SlRemoveEvent) => this.handleTagRemove(e, option)}>
${typeof tag === 'string' ? unsafeHTML(tag) : tag}
</div>`;
} else if (index === this.maxOptionsVisible) {
// Hit tag limit
return html`<sl-tag>+${this.selectedOptions.length - index}</sl-tag>`;
}
return html``;
});
}
private handleInvalid(event: Event) {
this.formControlController.setValidity(false);
@@ -755,37 +796,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
@blur=${this.handleBlur}
/>
${this.multiple
? html`
<div part="tags" class="select__tags">
${this.selectedOptions.map((option, index) => {
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
return html`
<sl-tag
part="tag"
exportparts="
base:tag__base,
content:tag__content,
remove-button:tag__remove-button,
remove-button__base:tag__remove-button__base
"
?pill=${this.pill}
size=${this.size}
removable
@sl-remove=${(event: SlRemoveEvent) => this.handleTagRemove(event, option)}
>
${option.getTextLabel()}
</sl-tag>
`;
} else if (index === this.maxOptionsVisible) {
return html` <sl-tag size=${this.size}> +${this.selectedOptions.length - index} </sl-tag> `;
} else {
return null;
}
})}
</div>
`
: ''}
${this.multiple ? html`<div part="tags" class="select__tags">${this.tags}</div>` : ''}
<input
class="select__value-input"

View File

@@ -168,20 +168,6 @@ export default class SlTree extends ShoelaceElement {
}
};
private syncTreeItems(selectedItem: SlTreeItem) {
const items = this.getAllTreeItems();
if (this.selection === 'multiple') {
syncCheckboxes(selectedItem);
} else {
for (const item of items) {
if (item !== selectedItem) {
item.selected = false;
}
}
}
}
private selectItem(selectedItem: SlTreeItem) {
const previousSelection = [...this.selectedItems];
@@ -190,12 +176,12 @@ export default class SlTree extends ShoelaceElement {
if (selectedItem.lazy) {
selectedItem.expanded = true;
}
this.syncTreeItems(selectedItem);
syncCheckboxes(selectedItem);
} else if (this.selection === 'single' || selectedItem.isLeaf) {
selectedItem.expanded = !selectedItem.expanded;
selectedItem.selected = true;
this.syncTreeItems(selectedItem);
const items = this.getAllTreeItems();
for (const item of items) {
item.selected = item === selectedItem;
}
} else if (this.selection === 'leaf') {
selectedItem.expanded = !selectedItem.expanded;
}
@@ -311,7 +297,7 @@ export default class SlTree extends ShoelaceElement {
return;
}
if (this.selection === 'multiple' && isExpandButton) {
if (isExpandButton) {
treeItem.expanded = !treeItem.expanded;
} else {
this.selectItem(treeItem);

View File

@@ -275,7 +275,6 @@ describe('<sl-tree>', () => {
// Assert
expect(el.selectedItems.length).to.eq(1);
expect(el.children[2]).to.have.attribute('selected');
expect(el.children[2]).to.have.attribute('expanded');
});
});
@@ -439,7 +438,6 @@ describe('<sl-tree>', () => {
await el.updateComplete;
// Assert
expect(node).to.have.attribute('selected');
expect(node).to.have.attribute('expanded');
});
});