diff --git a/.changeset/ten-monkeys-dress.md b/.changeset/ten-monkeys-dress.md
new file mode 100644
index 000000000..79aec66c6
--- /dev/null
+++ b/.changeset/ten-monkeys-dress.md
@@ -0,0 +1,5 @@
+---
+'@crowdstrike/glide-core': patch
+---
+
+Input's `type` attribute is now reflected.
diff --git a/.changeset/twenty-pandas-brake.md b/.changeset/twenty-pandas-brake.md
new file mode 100644
index 000000000..722d0781b
--- /dev/null
+++ b/.changeset/twenty-pandas-brake.md
@@ -0,0 +1,21 @@
+---
+'@crowdstrike/glide-core': minor
+---
+
+- Tab Panel no longer has an unused static `instanceCount` property.
+- Toggle no longer has a `name` property. `name` only applies to form controls and was unused.
+- Tree Item's `hasChildTreeItems` and `hasExpandIcon` properties and its `toggleExpand()` method have been marked private.
+
+Additionally, some internal changes were made to facillitate generating documentation programmatically forced us remove a few exported types and rename some custom properties:
+
+- Input no longer exports a `SUPPORTED_TYPES` interface.
+- Toasts no longer exports a `Toast` interface.
+- Tab Panel's custom properties have been renamed:
+
+ ```diff
+ - --panel-padding-inline-end
+ + --padding-inline-end
+
+ - --panel-padding-inline-start
+ + --padding-inline-start
+ ```
diff --git a/.husky/pre-push b/.husky/pre-push
index dee339b11..093f7af24 100755
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -1,4 +1,6 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
-NODE_ENV=production pnpm typecheck && NODE_ENV=production pnpm test
+NODE_ENV=production pnpm start
+NODE_ENV=production pnpm test
+NODE_ENV=production pnpm typecheck
diff --git a/.stylelintrc.js b/.stylelintrc.js
index 00e7d3ec5..667465faf 100644
--- a/.stylelintrc.js
+++ b/.stylelintrc.js
@@ -6,6 +6,7 @@ export default {
'stylelint-use-nesting',
'stylelint-use-logical',
'stylelint-order',
+ './dist/stylelint/plugin',
],
rules: {
// https://github.com/w3c/csswg-drafts/issues/9496
@@ -17,5 +18,6 @@ export default {
'no-descending-specificity': null,
'order/properties-alphabetical-order': true,
'prettier/prettier': true,
+ 'glide-core/no-unprefixed-private-custom-property': true,
},
};
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9bafe9a75..f4c59d164 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,11 +1,11 @@
{
- "eslint.workingDirectories": [{ "mode": "auto" }],
- "editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
- "stylelint.validate": ["css", "postcss", "typescript"],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
- }
+ },
+ "editor.formatOnSave": true,
+ "eslint.workingDirectories": [{ "mode": "auto" }],
+ "stylelint.validate": ["css", "postcss", "typescript"]
}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 33f0b4ba5..95322a617 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -146,44 +146,35 @@ Embrace encapsulation wherever you can.
#### Avoid styling `:host`
-Styling `:host` exposes the styles to consumers—allowing internal styles to be overridden.
-Due to that, we do not recommend styling `:host` in our components, but rather using CSS variables targeting the tag directly or using a class name.
+Styling `:host` exposes styles to consumers—allowing internal styles to be overridden.
+Style classes directly instead:
```css
/* ✅ -- GOOD */
-/* Target the button tag directly */
-button {
- background-color: var(--button-background-color);
-}
-
-/* Or use a class name `;
}
diff --git a/src/checkbox-group.styles.ts b/src/checkbox-group.styles.ts
index 1192caa30..90f7b6096 100644
--- a/src/checkbox-group.styles.ts
+++ b/src/checkbox-group.styles.ts
@@ -26,7 +26,7 @@ export default [
}
}
- .checkboxes {
+ .default-slot {
display: flex;
flex-direction: column;
grid-column: 2;
diff --git a/src/checkbox-group.ts b/src/checkbox-group.ts
index 8a33d90e0..cd16cef0a 100644
--- a/src/checkbox-group.ts
+++ b/src/checkbox-group.ts
@@ -46,8 +46,11 @@ export default class GlideCoreCheckboxGroup
static override styles = styles;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get disabled() {
+ get disabled(): boolean {
return this.#isDisabled;
}
@@ -76,8 +79,11 @@ export default class GlideCoreCheckboxGroup
@property()
privateSplit?: 'left' | 'middle';
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get required() {
+ get required(): boolean {
return this.#isRequired;
}
@@ -108,8 +114,11 @@ export default class GlideCoreCheckboxGroup
@property({ reflect: true })
tooltip?: string;
+ /**
+ * @default []
+ */
@property({ reflect: true, type: Array })
- get value() {
+ get value(): string[] {
return this.#value;
}
@@ -153,7 +162,7 @@ export default class GlideCoreCheckboxGroup
@property({ reflect: true })
readonly version = packageJson.version;
- checkValidity() {
+ checkValidity(): boolean {
this.isCheckingValidity = true;
const isValid = this.#internals.checkValidity();
this.isCheckingValidity = false;
@@ -186,11 +195,11 @@ export default class GlideCoreCheckboxGroup
}
}
- get form() {
+ get form(): HTMLFormElement | null {
return this.#internals.form;
}
- get validity() {
+ get validity(): ValidityState {
const isChecked = this.#checkboxes.some(({ checked }) => checked);
if (this.required && !isChecked) {
@@ -226,11 +235,11 @@ export default class GlideCoreCheckboxGroup
checkbox?.focus(options);
}
- formAssociatedCallback() {
+ formAssociatedCallback(): void {
this.form?.addEventListener('formdata', this.#onFormdata);
}
- formResetCallback() {
+ formResetCallback(): void {
for (const checkbox of this.#checkboxes) {
checkbox.formResetCallback();
}
@@ -265,12 +274,17 @@ export default class GlideCoreCheckboxGroup
})}
>
+ >
+
+
@@ -282,7 +296,12 @@ export default class GlideCoreCheckboxGroup
),
})}
name="description"
- >
+ >
+
+
${when(
this.#isShowValidationFeedback && this.validityMessage,
@@ -296,7 +315,7 @@ export default class GlideCoreCheckboxGroup
`;
}
- reportValidity() {
+ reportValidity(): boolean {
this.isReportValidityOrSubmit = true;
const isValid = this.#internals.reportValidity();
@@ -307,11 +326,11 @@ export default class GlideCoreCheckboxGroup
return isValid;
}
- resetValidityFeedback() {
+ resetValidityFeedback(): void {
this.isReportValidityOrSubmit = false;
}
- setCustomValidity(message: string) {
+ setCustomValidity(message: string): void {
this.validityMessage = message;
if (message === '') {
@@ -334,7 +353,7 @@ export default class GlideCoreCheckboxGroup
}
}
- setValidity(flags?: ValidityStateFlags, message?: string) {
+ setValidity(flags?: ValidityStateFlags, message?: string): void {
this.validityMessage = message;
// A validation message is required but unused because we disable native validation feedback.
diff --git a/src/checkbox.styles.ts b/src/checkbox.styles.ts
index 67f3a225c..65fd5d0bf 100644
--- a/src/checkbox.styles.ts
+++ b/src/checkbox.styles.ts
@@ -133,7 +133,7 @@ export default [
}
.checked-icon {
- --size: 0.75rem;
+ --private-size: 0.75rem;
align-items: center;
block-size: 100%;
diff --git a/src/checkbox.ts b/src/checkbox.ts
index 99e2dca59..f5d639808 100644
--- a/src/checkbox.ts
+++ b/src/checkbox.ts
@@ -50,8 +50,8 @@ export default class GlideCoreCheckbox
@property({ type: Boolean })
checked = false;
- @property({ attribute: 'internally-inert', type: Boolean })
- internallyInert = false;
+ @property({ attribute: 'private-internally-inert', type: Boolean })
+ privateInternallyInert = false;
@property({ reflect: true, type: Boolean })
disabled = false;
@@ -62,9 +62,12 @@ export default class GlideCoreCheckbox
@property({ type: Boolean })
indeterminate = false;
+ /**
+ * @default undefined
+ */
@property({ reflect: true })
@required
- get label() {
+ get label(): string | undefined {
return this.#label;
}
@@ -111,7 +114,7 @@ export default class GlideCoreCheckbox
// Private because it's only meant to be used by Dropdown.
@property({ attribute: 'private-size' })
- privateSize: 'large' | 'small' = 'large';
+ privateSize: 'small' | 'large' = 'large';
// Private because it's only meant to be used by Form Controls Layout.
@property()
@@ -130,8 +133,11 @@ export default class GlideCoreCheckbox
@property({ reflect: true })
tooltip?: string;
+ /**
+ * @default undefined
+ */
@property({ reflect: true })
- get value() {
+ get value(): string {
return this.#value;
}
@@ -161,11 +167,11 @@ export default class GlideCoreCheckbox
@property({ reflect: true })
readonly version = packageJson.version;
- get form() {
+ get form(): HTMLFormElement | null {
return this.#internals.form;
}
- checkValidity() {
+ checkValidity(): boolean {
this.isCheckingValidity = true;
const isValid = this.#internals.checkValidity();
this.isCheckingValidity = false;
@@ -200,7 +206,7 @@ export default class GlideCoreCheckbox
this.#intersectionObserver?.disconnect();
}
- get validity() {
+ get validity(): ValidityState {
// If we're in a Checkbox Group, `disabled`, `required`, and whether or not
// the form has been submitted don't apply because Checkbox Group handles those
// states for the group as a whole.
@@ -245,11 +251,11 @@ export default class GlideCoreCheckbox
this.#inputElementRef.value?.focus(options);
}
- formAssociatedCallback() {
+ formAssociatedCallback(): void {
this.form?.addEventListener('formdata', this.#onFormdata);
}
- formResetCallback() {
+ formResetCallback(): void {
this.checked = this.getAttribute('checked') === '';
this.indeterminate = this.getAttribute('indeterminate') === '';
}
@@ -273,7 +279,7 @@ export default class GlideCoreCheckbox
data-test="input"
type="checkbox"
.checked=${this.checked}
- .inert=${this.internallyInert}
+ .inert=${this.privateInternallyInert}
?disabled=${this.disabled}
?required=${this.required}
@change=${this.#onInputChangeOrInput}
@@ -294,7 +300,9 @@ export default class GlideCoreCheckbox
-
+
+
+
+
+ >
+
+
+
${when(
this.#isShowValidationFeedback && this.validityMessage,
() =>
@@ -399,7 +417,7 @@ export default class GlideCoreCheckbox
`;
}
- reportValidity() {
+ reportValidity(): boolean {
this.privateIsReportValidityOrSubmit = true;
const isValid = this.#internals.reportValidity();
@@ -409,11 +427,11 @@ export default class GlideCoreCheckbox
return isValid;
}
- resetValidityFeedback() {
+ resetValidityFeedback(): void {
this.privateIsReportValidityOrSubmit = false;
}
- setCustomValidity(message: string) {
+ setCustomValidity(message: string): void {
this.validityMessage = message;
if (message === '') {
@@ -436,7 +454,7 @@ export default class GlideCoreCheckbox
}
}
- setValidity(flags?: ValidityStateFlags, message?: string) {
+ setValidity(flags?: ValidityStateFlags, message?: string): void {
this.validityMessage = message;
this.#internals.setValidity(flags, ' ', this.#inputElementRef.value);
}
@@ -573,7 +591,7 @@ export default class GlideCoreCheckbox
// Unlike "input" events, "change" events aren't composed. So we have to
// manually dispatch them.
this.dispatchEvent(
- new Event(event.type, { bubbles: true, composed: true }),
+ new Event('change', { bubbles: true, composed: true }),
);
}
}
diff --git a/src/drawer.styles.ts b/src/drawer.styles.ts
index 9def92da5..d87ac049e 100644
--- a/src/drawer.styles.ts
+++ b/src/drawer.styles.ts
@@ -2,6 +2,11 @@ import { css } from 'lit';
export default [
css`
+ :host {
+ /* The width the drawer */
+ --width: 27.375rem;
+ }
+
.component {
background-color: var(--glide-core-surface-base-xlightest);
block-size: 0;
@@ -30,7 +35,7 @@ export default [
.open {
backdrop-filter: blur(50px);
block-size: auto;
- inline-size: var(--width, 27.375rem);
+ inline-size: var(--width);
inset: 0 0 0 auto;
visibility: visible;
}
diff --git a/src/drawer.ts b/src/drawer.ts
index 3b54d4bae..5c16963cd 100644
--- a/src/drawer.ts
+++ b/src/drawer.ts
@@ -40,8 +40,11 @@ export default class GlideCoreDrawer extends LitElement {
@property({ reflect: true, type: Boolean })
pinned = false;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get open() {
+ get open(): boolean {
return this.#isOpen;
}
@@ -166,7 +169,14 @@ export default class GlideCoreDrawer extends LitElement {
tabindex="-1"
${ref(this.#componentElementRef)}
>
-
+
+
+
`;
}
diff --git a/src/dropdown.option.styles.ts b/src/dropdown.option.styles.ts
index 7412e4f6e..f4db28f68 100644
--- a/src/dropdown.option.styles.ts
+++ b/src/dropdown.option.styles.ts
@@ -152,7 +152,7 @@ export default [
}
.checked-icon-container {
- --size: 1rem;
+ --private-size: 1rem;
display: contents;
diff --git a/src/dropdown.option.ts b/src/dropdown.option.ts
index 2a9aa7afa..3f8aec931 100644
--- a/src/dropdown.option.ts
+++ b/src/dropdown.option.ts
@@ -35,8 +35,11 @@ export default class GlideCoreDropdownOption extends LitElement {
static override styles = styles;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get disabled() {
+ get disabled(): boolean {
return this.#isDisabled;
}
@@ -45,8 +48,11 @@ export default class GlideCoreDropdownOption extends LitElement {
this.#isDisabled = isDisabled;
}
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get editable() {
+ get editable(): boolean {
return this.#isEditable;
}
@@ -60,9 +66,12 @@ export default class GlideCoreDropdownOption extends LitElement {
);
}
+ /**
+ * @default undefined
+ */
@property({ reflect: true })
@required
- get label() {
+ get label(): string | undefined {
return this.#label;
}
@@ -95,8 +104,11 @@ export default class GlideCoreDropdownOption extends LitElement {
@property({ attribute: 'private-multiple', type: Boolean })
privateMultiple = false;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get selected() {
+ get selected(): boolean {
return this.#selected;
}
@@ -206,8 +218,11 @@ export default class GlideCoreDropdownOption extends LitElement {
}
}
+ /**
+ * @default ''
+ */
@property({ reflect: true })
- get value() {
+ get value(): string {
return this.#value;
}
@@ -229,6 +244,10 @@ export default class GlideCoreDropdownOption extends LitElement {
this.#value = value;
}
+ privateEdit() {
+ this.dispatchEvent(new Event('edit', { bubbles: true, composed: true }));
+ }
+
async privateUpdateCheckbox() {
// Hacky indeed. This is for the case where Dropdown is set programmatically
// from a single to a multiselect. `this.isMultiple` is set to `true` but
@@ -246,7 +265,6 @@ export default class GlideCoreDropdownOption extends LitElement {
// The linter wants a keyboard handler. There's one on Dropdown itself. It's there
// because options aren't focusable and thus don't produce keyboard events when Dropdown
// is filterable.
-
return html`
+ >
+
+
${when(this.editable, () => {
@@ -323,7 +346,12 @@ export default class GlideCoreDropdownOption extends LitElement {
+ })} name="icon">
+
+
{
return this.#optionElements.filter(({ label }) => {
@@ -416,11 +436,11 @@ export default class GlideCoreDropdown
}
}
- get form() {
+ get form(): HTMLFormElement | null {
return this.#internals.form;
}
- get validity() {
+ get validity(): ValidityState {
if (this.required && this.selectedOptions.length === 0) {
// A validation message is required but unused because we disable native validation feedback.
// And an empty string isn't allowed. Thus a single space.
@@ -447,11 +467,11 @@ export default class GlideCoreDropdown
return this.#internals.validity;
}
- formAssociatedCallback() {
+ formAssociatedCallback(): void {
this.form?.addEventListener('formdata', this.#onFormdata);
}
- formResetCallback() {
+ formResetCallback(): void {
for (const option of this.#optionElements) {
const isInitiallySelected = option.hasAttribute('selected');
@@ -587,7 +607,15 @@ export default class GlideCoreDropdown
data-test="multiselect-icon-slot"
name="icon:${value}"
slot="icon"
- >
+ >
+
+
`;
})}
@@ -604,7 +632,12 @@ export default class GlideCoreDropdown
})}
data-test="single-select-icon-slot"
name="icon:${this.selectedOptions.at(0)?.value}"
- >`;
+ >
+
+ `;
})}
+ >
+
+
${when(this.isNoResults, () => {
@@ -880,7 +918,13 @@ export default class GlideCoreDropdown
),
})}
name="description"
- >
+ >
+
+
+
${when(
this.#isShowValidationFeedback && this.validityMessage,
() =>
@@ -893,9 +937,8 @@ export default class GlideCoreDropdown
`;
}
- reportValidity() {
+ reportValidity(): boolean {
this.isReportValidityOrSubmit = true;
-
const isValid = this.#internals.reportValidity();
// Ensures that getters referencing this.validity?.valid update (i.e. #isShowValidationFeedback)
@@ -904,11 +947,11 @@ export default class GlideCoreDropdown
return isValid;
}
- resetValidityFeedback() {
+ resetValidityFeedback(): void {
this.isReportValidityOrSubmit = false;
}
- setCustomValidity(message: string) {
+ setCustomValidity(message: string): void {
this.validityMessage = message;
if (message === '') {
@@ -935,7 +978,7 @@ export default class GlideCoreDropdown
}
}
- setValidity(flags?: ValidityStateFlags, message?: string) {
+ setValidity(flags?: ValidityStateFlags, message?: string): void {
this.validityMessage = message;
// A validation message is required but unused because we disable native validation feedback.
@@ -1430,9 +1473,7 @@ export default class GlideCoreDropdown
// A "click" event, on the other hand, is dispatched when an Edit button is
// clicked. So Dropdown Option could dispatch "edit" in that case. But then
// two components instead of one would be responsible for dispatching "edit".
- this.activeOption.dispatchEvent(
- new Event('edit', { bubbles: true, composed: true }),
- );
+ this.activeOption.privateEdit();
this.open = false;
@@ -1650,9 +1691,7 @@ export default class GlideCoreDropdown
event.target instanceof Node &&
this.#editButtonElementRef.value?.contains(event.target)
) {
- this.selectedOptions[0].dispatchEvent(
- new Event('edit', { bubbles: true, composed: true }),
- );
+ this.selectedOptions[0].privateEdit();
return;
}
@@ -1939,10 +1978,7 @@ export default class GlideCoreDropdown
option instanceof GlideCoreDropdownOption &&
option.privateIsEditActive
) {
- option.dispatchEvent(
- new Event('edit', { bubbles: true, composed: true }),
- );
-
+ option.privateEdit();
this.open = false;
return;
@@ -2417,8 +2453,8 @@ const icons = {
viewBox="0 0 16 16"
fill="none"
style=${styleMap({
- height: 'var(--size)',
- width: 'var(--size)',
+ height: 'var(--private-size)',
+ width: 'var(--private-size)',
})}
>
(
(name) =>
- `https://github.com/CrowdStrike/glide-core/blob/main/packages/eslint-plugin/src/rules/${name}.ts`,
+ `https://github.com/CrowdStrike/glide-core/blob/main/src/eslint/rules/${name}.ts`,
);
export const consistentReferenceElementDeclarations = createRule({
diff --git a/src/eslint/rules/consistent-test-fixture-variable-declarator.ts b/src/eslint/rules/consistent-test-fixture-variable-declarator.ts
index 138dab72c..1b5df289c 100644
--- a/src/eslint/rules/consistent-test-fixture-variable-declarator.ts
+++ b/src/eslint/rules/consistent-test-fixture-variable-declarator.ts
@@ -5,7 +5,7 @@ const createRule = ESLintUtils.RuleCreator<{
recommended: boolean;
}>(
(name) =>
- `https://github.com/CrowdStrike/glide-core/blob/main/packages/eslint-plugin/src/rules/${name}.ts`,
+ `https://github.com/CrowdStrike/glide-core/blob/main/src/eslint/rules/${name}.ts`,
);
export const consistentTestFixtureVariableDeclarator = createRule({
@@ -27,7 +27,7 @@ export const consistentTestFixtureVariableDeclarator = createRule({
create(context) {
return {
VariableDeclarator(node) {
- const isAFixture =
+ const isFixture =
node.init &&
node.init.type === AST_NODE_TYPES.AwaitExpression &&
node.init.argument &&
@@ -37,7 +37,7 @@ export const consistentTestFixtureVariableDeclarator = createRule({
node.init.argument.callee.name === 'fixture' &&
node.init.argument.arguments?.length > 0;
- if (!isAFixture) {
+ if (!isFixture) {
return;
}
diff --git a/src/eslint/rules/event-dispatch-from-this.test.ts b/src/eslint/rules/event-dispatch-from-this.test.ts
new file mode 100644
index 000000000..41f9ea5ee
--- /dev/null
+++ b/src/eslint/rules/event-dispatch-from-this.test.ts
@@ -0,0 +1,48 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { eventDispatchFromThis } from './event-dispatch-from-this.js';
+
+const ruleTester = new RuleTester();
+
+ruleTester.run('event-dispatch-from-this', eventDispatchFromThis, {
+ valid: [
+ {
+ code: `
+ export default class {
+ method() {
+ this.dispatchEvent(new Event('change'))
+ }
+ }
+ `,
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ export default class {
+ method() {
+ this.element.dispatchEvent(new Event('change'))
+ }
+ }
+ `,
+ errors: [
+ {
+ messageId: 'dispatchFromThis',
+ },
+ ],
+ },
+ {
+ code: `
+ export default class {
+ method() {
+ document.querySelector('input').dispatchEvent(new Event('change'))
+ }
+ }
+ `,
+ errors: [
+ {
+ messageId: 'dispatchFromThis',
+ },
+ ],
+ },
+ ],
+});
diff --git a/src/eslint/rules/event-dispatch-from-this.ts b/src/eslint/rules/event-dispatch-from-this.ts
new file mode 100644
index 000000000..5cfba2f1d
--- /dev/null
+++ b/src/eslint/rules/event-dispatch-from-this.ts
@@ -0,0 +1,42 @@
+import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
+
+// https://github.com/typescript-eslint/typescript-eslint/blob/main/docs/developers/Custom_Rules.mdx?plain=1#L109
+const createRule = ESLintUtils.RuleCreator<{
+ recommended: boolean;
+}>((name) => {
+ return `https://github.com/CrowdStrike/glide-core/blob/main/src/eslint/rules/${name}.ts`;
+});
+
+export const eventDispatchFromThis = createRule({
+ name: 'event-dispatch-from-this',
+ meta: {
+ docs: {
+ description: 'Ensures events are dispatched directly from `this`.',
+ recommended: true,
+ },
+ type: 'suggestion',
+ messages: {
+ dispatchFromThis:
+ 'Dispatch events directly from `this` to help us populate our elements manifest with events.',
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (
+ node.callee.type === AST_NODE_TYPES.MemberExpression &&
+ node.callee.property.type === AST_NODE_TYPES.Identifier &&
+ node.callee.property.name === 'dispatchEvent' &&
+ node.callee.object.type !== AST_NODE_TYPES.ThisExpression
+ ) {
+ context.report({
+ node,
+ messageId: 'dispatchFromThis',
+ });
+ }
+ },
+ };
+ },
+});
diff --git a/src/eslint/rules/no-glide-core-prefixed-event-name.ts b/src/eslint/rules/no-glide-core-prefixed-event-name.ts
index a0801ab68..9c13785c0 100644
--- a/src/eslint/rules/no-glide-core-prefixed-event-name.ts
+++ b/src/eslint/rules/no-glide-core-prefixed-event-name.ts
@@ -28,10 +28,12 @@ export const noPrefixedEventName = createRule({
create(context) {
return {
NewExpression(node) {
- if (
+ const isEvent =
node.callee.type === AST_NODE_TYPES.Identifier &&
- (node.callee.name === 'Event' ||
- node.callee.name === 'CustomEvent') &&
+ ['Event', 'CustomEvent'].includes(node.callee.name);
+
+ if (
+ isEvent &&
node.arguments.length > 0 &&
node.arguments[0].type === AST_NODE_TYPES.Literal &&
typeof node.arguments[0].value === 'string' &&
@@ -42,7 +44,7 @@ export const noPrefixedEventName = createRule({
context.report({
node,
messageId: 'noPrefix',
- fix: function (fixer) {
+ fix(fixer) {
return fixer.replaceText(
node.arguments[0],
`'${eventName.replace('glide-core-', '')}'`,
diff --git a/src/eslint/rules/no-redundant-property-attribute.ts b/src/eslint/rules/no-redundant-property-attribute.ts
index 33c6b9df0..5517a4c92 100644
--- a/src/eslint/rules/no-redundant-property-attribute.ts
+++ b/src/eslint/rules/no-redundant-property-attribute.ts
@@ -87,7 +87,7 @@ export const noRedudantPropertyAttribute = createRule({
context.report({
node: property,
messageId: 'noRedudantPropertyAttribute',
- fix: function (fixer) {
+ fix(fixer) {
if (argument.properties?.length === 1) {
const source = context.sourceCode;
const tokenBefore = source.getTokenBefore(argument);
diff --git a/src/eslint/rules/no-redundant-property-string-type.ts b/src/eslint/rules/no-redundant-property-string-type.ts
index 59c1019c3..858947b9b 100644
--- a/src/eslint/rules/no-redundant-property-string-type.ts
+++ b/src/eslint/rules/no-redundant-property-string-type.ts
@@ -69,7 +69,7 @@ export const noRedudantPropertyStringType = createRule({
context.report({
node: property,
messageId: 'noRedudantPropertyStringType',
- fix: function (fixer) {
+ fix(fixer) {
if (argument.properties?.length === 1) {
const source = context.sourceCode;
const tokenBefore = source.getTokenBefore(argument);
diff --git a/src/eslint/rules/no-space-press.ts b/src/eslint/rules/no-space-press.ts
index ff6d6f0b1..4e46ec8ce 100644
--- a/src/eslint/rules/no-space-press.ts
+++ b/src/eslint/rules/no-space-press.ts
@@ -39,7 +39,7 @@ export const noSpacePress = createRule({
context.report({
node,
messageId: 'preferWhitespace',
- fix: function (fixer) {
+ fix(fixer) {
return fixer.replaceText(node.right, "' '");
},
});
@@ -68,7 +68,7 @@ export const noSpacePress = createRule({
context.report({
node: property,
messageId: 'preferWhitespace',
- fix: function (fixer) {
+ fix(fixer) {
return fixer.replaceText(property.value, "' '");
},
});
diff --git a/src/eslint/rules/prefer-to-be-true-or-false.ts b/src/eslint/rules/prefer-to-be-true-or-false.ts
index 9d438c136..29a070682 100644
--- a/src/eslint/rules/prefer-to-be-true-or-false.ts
+++ b/src/eslint/rules/prefer-to-be-true-or-false.ts
@@ -50,7 +50,7 @@ export const preferToBeTrueOrFalse = createRule({
context.report({
node,
messageId: 'preferToBeFalse',
- fix: function (fixer) {
+ fix(fixer) {
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
return null;
}
@@ -79,7 +79,7 @@ export const preferToBeTrueOrFalse = createRule({
context.report({
node,
messageId: 'preferToBeTrue',
- fix: function (fixer) {
+ fix(fixer) {
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
return null;
}
diff --git a/src/eslint/rules/prefixed-lit-element-class-declaration.ts b/src/eslint/rules/prefixed-lit-element-class-declaration.ts
index 42e4fcfc3..c08ac7c3b 100644
--- a/src/eslint/rules/prefixed-lit-element-class-declaration.ts
+++ b/src/eslint/rules/prefixed-lit-element-class-declaration.ts
@@ -37,7 +37,7 @@ export const prefixedClassDeclaration = createRule({
context.report({
node,
messageId: 'addPrefix',
- fix: function (fixer) {
+ fix(fixer) {
const nodeId = node.id;
if (!nodeId) {
diff --git a/src/eslint/rules/public-getter-default-comment.test.ts b/src/eslint/rules/public-getter-default-comment.test.ts
new file mode 100644
index 000000000..51e6ca8ee
--- /dev/null
+++ b/src/eslint/rules/public-getter-default-comment.test.ts
@@ -0,0 +1,106 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { publicGetterDefaultComment } from './public-getter-default-comment.js';
+
+const ruleTester = new RuleTester();
+
+ruleTester.run('public-getter-default-comment', publicGetterDefaultComment, {
+ valid: [
+ {
+ code: `
+ export default class {
+ /**
+ * @default {string}
+ */
+ get property() {}
+ set property() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ get #property() {}
+ set #property() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ private get property() {}
+ private set property() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ get privateProperty() {}
+ set privateProperty() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ override get property() {}
+ override set property() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ override get property() {}
+ }
+ `,
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ export default class {
+ /**
+ * Description
+ */
+ get property() {}
+ set property() {}
+ }
+ `,
+ errors: [
+ {
+ messageId: 'addDefaultTag',
+ },
+ ],
+ },
+ {
+ code: `
+ export default class {
+ get property() {}
+ set property() {}
+ }
+ `,
+ errors: [
+ {
+ messageId: 'addCommentAndDefaultTag',
+ },
+ ],
+ },
+ {
+ code: `
+ export default class {
+ /*
+ * @default {string}
+ */
+ get property() {}
+ set property() {}
+ }
+ `,
+ errors: [
+ {
+ messageId: 'useJsDocComment',
+ },
+ ],
+ },
+ ],
+});
diff --git a/src/eslint/rules/public-getter-default-comment.ts b/src/eslint/rules/public-getter-default-comment.ts
new file mode 100644
index 000000000..2281422d5
--- /dev/null
+++ b/src/eslint/rules/public-getter-default-comment.ts
@@ -0,0 +1,111 @@
+import {
+ AST_NODE_TYPES,
+ AST_TOKEN_TYPES,
+ ESLintUtils,
+} from '@typescript-eslint/utils';
+import { parse as commentParser } from 'comment-parser';
+
+// https://github.com/typescript-eslint/typescript-eslint/blob/main/docs/developers/Custom_Rules.mdx?plain=1#L109
+const createRule = ESLintUtils.RuleCreator<{
+ recommended: boolean;
+}>((name) => {
+ return `https://github.com/CrowdStrike/glide-core/blob/main/src/eslint/rules/${name}.ts`;
+});
+
+export const publicGetterDefaultComment = createRule({
+ name: 'public-getter-default-comment',
+ meta: {
+ docs: {
+ description:
+ 'Ensures getters that have setters have a comment with a `@default` tag.',
+ recommended: true,
+ },
+ type: 'suggestion',
+ messages: {
+ addDefaultTag:
+ 'Add a "@default" tag to your comment to help us populate our elements manifest with the correct default type.',
+ addCommentAndDefaultTag:
+ 'Add a JSDoc comment with a "@default" tag to help us populate our elements manifest with the correct default type.',
+ useJsDocComment:
+ 'Use a JSDoc-style comment to help us populate our elements manifest with the correct default type.',
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+ create(context) {
+ const comments = context.sourceCode.getAllComments();
+
+ return {
+ MethodDefinition(node) {
+ const isPseudoPrivate =
+ node.key.type === AST_NODE_TYPES.Identifier &&
+ node.key.name.startsWith('private');
+
+ const hasSetter = node.parent.body.some((classElement) => {
+ return (
+ node.key.type === AST_NODE_TYPES.Identifier &&
+ node.kind === 'get' &&
+ classElement.type === AST_NODE_TYPES.MethodDefinition &&
+ classElement.kind === 'set' &&
+ classElement.key.type === AST_NODE_TYPES.Identifier &&
+ classElement.key.name === node.key.name
+ );
+ });
+
+ if (
+ hasSetter &&
+ node.kind === 'get' &&
+ node.key.type !== AST_NODE_TYPES.PrivateIdentifier &&
+ node.accessibility !== 'private' &&
+ !isPseudoPrivate &&
+ !node.override
+ ) {
+ const comment = comments.find(({ loc, type }) => {
+ return (
+ type === AST_TOKEN_TYPES.Block &&
+ loc.end.line === node.loc.start.line - 1
+ );
+ });
+
+ if (comment) {
+ const parsed = commentParser(`/**
+ ${comment.value}
+ */`);
+
+ const hasDefaultTag = parsed.at(0)?.tags.some((tag) => {
+ return tag.tag === 'default';
+ });
+
+ /*
+ * This is block comment. But it's not a JSDoc one.
+ */
+
+ /**
+ * This is a JSDoc one.
+ */
+ if (hasDefaultTag && !comment.value.startsWith('*')) {
+ context.report({
+ node,
+ messageId: 'useJsDocComment',
+ });
+ }
+
+ if (!hasDefaultTag) {
+ context.report({
+ node,
+ messageId: 'addDefaultTag',
+ });
+ }
+
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: 'addCommentAndDefaultTag',
+ });
+ }
+ },
+ };
+ },
+});
diff --git a/src/eslint/rules/public-member-return-type.test.ts b/src/eslint/rules/public-member-return-type.test.ts
new file mode 100644
index 000000000..c01274025
--- /dev/null
+++ b/src/eslint/rules/public-member-return-type.test.ts
@@ -0,0 +1,72 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { publicMemberReturnType } from './public-member-return-type.js';
+
+const ruleTester = new RuleTester();
+
+ruleTester.run('public-member-return-type', publicMemberReturnType, {
+ valid: [
+ {
+ code: `
+ export default class {
+ method(): boolean {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ #method() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ private method() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ privateMethod() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ constructor() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ override method() {}
+ }
+ `,
+ },
+ {
+ code: `
+ export default class {
+ set property() {}
+ }
+ `,
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ export default class {
+ method() {}
+ }
+ `,
+ errors: [
+ {
+ messageId: 'addReturnType',
+ },
+ ],
+ },
+ ],
+});
diff --git a/src/eslint/rules/public-member-return-type.ts b/src/eslint/rules/public-member-return-type.ts
new file mode 100644
index 000000000..5c50fd07d
--- /dev/null
+++ b/src/eslint/rules/public-member-return-type.ts
@@ -0,0 +1,50 @@
+import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
+
+// https://github.com/typescript-eslint/typescript-eslint/blob/main/docs/developers/Custom_Rules.mdx?plain=1#L109
+const createRule = ESLintUtils.RuleCreator<{
+ recommended: boolean;
+}>((name) => {
+ return `https://github.com/CrowdStrike/glide-core/blob/main/src/eslint/rules/${name}.ts`;
+});
+
+export const publicMemberReturnType = createRule({
+ name: 'public-member-return-type',
+ meta: {
+ docs: {
+ description:
+ 'Ensures an explicit return type on public methods and getters with setters.',
+ recommended: true,
+ },
+ type: 'suggestion',
+ messages: {
+ addReturnType:
+ 'Add an explicit return type to help us populate our elements manifest with the correct return type.',
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+ create(context) {
+ return {
+ MethodDefinition(node) {
+ const isPseudoPrivate =
+ node.key.type === AST_NODE_TYPES.Identifier &&
+ node.key.name.startsWith('private');
+
+ if (
+ node.kind !== 'constructor' &&
+ node.kind !== 'set' &&
+ node.key.type !== AST_NODE_TYPES.PrivateIdentifier &&
+ node.accessibility !== 'private' &&
+ !isPseudoPrivate &&
+ !node.override &&
+ !node.value.returnType
+ ) {
+ context.report({
+ node,
+ messageId: 'addReturnType',
+ });
+ }
+ },
+ };
+ },
+});
diff --git a/src/eslint/rules/slot-type-comment.test.ts b/src/eslint/rules/slot-type-comment.test.ts
new file mode 100644
index 000000000..ad6ae474f
--- /dev/null
+++ b/src/eslint/rules/slot-type-comment.test.ts
@@ -0,0 +1,77 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { slotTypeComment } from './slot-type-comment.js';
+
+const ruleTester = new RuleTester();
+const classMap = () => null;
+
+ruleTester.run('slot-type-comment', slotTypeComment, {
+ valid: [
+ {
+ code: `
+ export default class {
+ render() {
+ return html\`
+
+
+
+ \`
+ }
+ }
+ `,
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ export default class {
+ render() {
+ return html\`
+
+ \`
+ }
+ }
+ `,
+ errors: [
+ {
+ messageId: 'addSlotComment',
+ },
+ ],
+ },
+ {
+ code: `
+ export default class {
+ render() {
+ return html\`
+
+
+
+ \`
+ }
+ }
+ `,
+ errors: [
+ {
+ messageId: 'addSlotTypeComment',
+ },
+ ],
+ },
+ {
+ code: `
+ export default class {
+ render() {
+ return html\`
+
+
+
+ \`
+ }
+ }
+ `,
+ errors: [
+ {
+ messageId: 'addSlotTypeCommentType',
+ },
+ ],
+ },
+ ],
+});
diff --git a/src/eslint/rules/slot-type-comment.ts b/src/eslint/rules/slot-type-comment.ts
new file mode 100644
index 000000000..45aff2938
--- /dev/null
+++ b/src/eslint/rules/slot-type-comment.ts
@@ -0,0 +1,96 @@
+import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
+import { parse as htmlParser, CommentNode } from 'node-html-parser';
+import { parse as commentParser } from 'comment-parser';
+
+// https://github.com/typescript-eslint/typescript-eslint/blob/main/docs/developers/Custom_Rules.mdx?plain=1#L109
+const createRule = ESLintUtils.RuleCreator<{
+ recommended: boolean;
+}>((name) => {
+ return `https://github.com/CrowdStrike/glide-core/blob/main/src/eslint/rules/${name}.ts`;
+});
+
+export const slotTypeComment = createRule({
+ name: 'slot-type-comment',
+ meta: {
+ docs: {
+ description: 'Ensures slots have a `@type` comment.',
+ recommended: true,
+ },
+ type: 'suggestion',
+ messages: {
+ addSlotComment:
+ 'Add a comment inside your slot and include a `@type` tag.',
+ addSlotTypeComment: 'Add a `@type` tag to your slot comment.',
+ addSlotTypeCommentType:
+ "Add a type to your slot's `@type` tag. Check that the type is wrapped in curlies.",
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+ create(context) {
+ return {
+ TaggedTemplateExpression(node) {
+ if (
+ node.tag.type === AST_NODE_TYPES.Identifier &&
+ node.tag.name === 'html'
+ ) {
+ // Reducers are often inscrutable. But I thought this was a good case for
+ // one.
+ //
+ // A quasi is the string part of a template literal. When a template literal
+ // contains an expression, it's broken into a array of quasis, which we then
+ // have to recombine to get the entirety of the markup.
+ //
+ // eslint-disable-next-line unicorn/no-array-reduce
+ const markup = node.quasi.quasis.reduce((accumulator, quasi) => {
+ accumulator += quasi.value.raw;
+ return accumulator;
+ }, '');
+
+ const root = htmlParser(markup, {
+ comment: true,
+ });
+
+ for (const slot of root.querySelectorAll('slot')) {
+ const comment = slot.childNodes.find((node) => {
+ return node instanceof CommentNode;
+ });
+
+ if (!comment) {
+ context.report({
+ node: node.quasi,
+ messageId: 'addSlotComment',
+ });
+
+ return;
+ }
+
+ const tags = commentParser(`/**
+ ${comment.rawText}
+ */`).flatMap((block) => block.tags);
+
+ const typeTag = tags.find((tag) => tag.tag === 'type');
+
+ if (!typeTag) {
+ context.report({
+ node: node.quasi,
+ messageId: 'addSlotTypeComment',
+ });
+
+ return;
+ }
+
+ if (typeTag.type === '') {
+ context.report({
+ node: node.quasi,
+ messageId: 'addSlotTypeCommentType',
+ });
+
+ return;
+ }
+ }
+ }
+ },
+ };
+ },
+});
diff --git a/src/eslint/rules/string-event-name.test.ts b/src/eslint/rules/string-event-name.test.ts
new file mode 100644
index 000000000..e87107055
--- /dev/null
+++ b/src/eslint/rules/string-event-name.test.ts
@@ -0,0 +1,43 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { stringEventName } from './string-event-name.js';
+
+const ruleTester = new RuleTester();
+
+ruleTester.run('string-event-name', stringEventName, {
+ valid: [
+ {
+ code: `
+ this.dispatchEvent(new Event('change'));
+ `,
+ },
+ {
+ code: `
+ this.dispatchEvent(new CustomEvent('change'));
+ `,
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ const variable = 'change';
+ this.dispatchEvent(new Event(variable));
+ `,
+ errors: [
+ {
+ messageId: 'stringEventName',
+ },
+ ],
+ },
+ {
+ code: `
+ const variable = 'change';
+ this.dispatchEvent(new CustomEvent(variable));
+ `,
+ errors: [
+ {
+ messageId: 'stringEventName',
+ },
+ ],
+ },
+ ],
+});
diff --git a/src/eslint/rules/string-event-name.ts b/src/eslint/rules/string-event-name.ts
new file mode 100644
index 000000000..3095209b9
--- /dev/null
+++ b/src/eslint/rules/string-event-name.ts
@@ -0,0 +1,44 @@
+import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
+
+// https://github.com/typescript-eslint/typescript-eslint/blob/main/docs/developers/Custom_Rules.mdx?plain=1#L109
+const createRule = ESLintUtils.RuleCreator<{
+ recommended: boolean;
+}>((name) => {
+ return `https://github.com/CrowdStrike/glide-core/blob/main/src/eslint/rules/${name}.ts`;
+});
+
+export const stringEventName = createRule({
+ name: 'string-event-name',
+ meta: {
+ docs: {
+ description: 'Ensures events names are strings.',
+ recommended: true,
+ },
+ type: 'suggestion',
+ messages: {
+ stringEventName:
+ 'Use a string for the event name to help us populate our elements manifest with events.',
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+ create(context) {
+ return {
+ NewExpression(node) {
+ const isRelevantConstructor =
+ node.callee.type === AST_NODE_TYPES.Identifier &&
+ ['Event', 'CustomEvent'].includes(node.callee.name);
+
+ if (
+ isRelevantConstructor &&
+ node.arguments.at(0)?.type !== AST_NODE_TYPES.Literal
+ ) {
+ context.report({
+ node,
+ messageId: 'stringEventName',
+ });
+ }
+ },
+ };
+ },
+});
diff --git a/src/form-controls-layout.ts b/src/form-controls-layout.ts
index 93e0bb784..57488e5ed 100644
--- a/src/form-controls-layout.ts
+++ b/src/form-controls-layout.ts
@@ -32,8 +32,11 @@ export default class GlideCoreFormControlsLayout extends LitElement {
static override styles = styles;
+ /**
+ * @default 'left'
+ */
@property({ reflect: true })
- get split() {
+ get split(): 'left' | 'middle' {
return this.#split;
}
@@ -65,7 +68,9 @@ export default class GlideCoreFormControlsLayout extends LitElement {
GlideCoreTextArea,
])}
${ref(this.#slotElementRef)}
- >
+ >
+
+
`;
}
diff --git a/src/icon-button.styles.ts b/src/icon-button.styles.ts
index 3a9ddbeb3..ab3e12395 100644
--- a/src/icon-button.styles.ts
+++ b/src/icon-button.styles.ts
@@ -13,14 +13,14 @@ export default [
.component {
align-items: center;
- block-size: var(--size, 1.625rem);
+ block-size: var(--private-size, 1.625rem);
border-color: transparent;
border-radius: 0.5rem;
border-style: solid;
border-width: 1px;
cursor: pointer;
display: inline-flex;
- inline-size: var(--size, 1.625rem);
+ inline-size: var(--private-size, 1.625rem);
justify-content: center;
padding-inline: 0;
transition-duration: 150ms;
@@ -39,7 +39,7 @@ export default [
&.primary {
background-color: var(--glide-core-surface-primary);
border-color: transparent;
- color: var(--icon-color, var(--glide-core-icon-selected));
+ color: var(--private-icon-color, var(--glide-core-icon-selected));
&:disabled {
background-color: var(--glide-core-surface-base-gray-light);
@@ -64,7 +64,7 @@ export default [
&.secondary {
background-color: transparent;
border-color: transparent;
- color: var(--icon-color, var(--glide-core-icon-default));
+ color: var(--private-icon-color, var(--glide-core-icon-default));
&:disabled {
background-color: transparent;
@@ -86,10 +86,10 @@ export default [
&.tertiary {
background-color: transparent;
- block-size: var(--size, 1rem);
+ block-size: var(--private-size, 1rem);
border-color: transparent;
- color: var(--icon-color, var(--glide-core-icon-default));
- inline-size: var(--size, 1rem);
+ color: var(--private-icon-color, var(--glide-core-icon-default));
+ inline-size: var(--private-size, 1rem);
padding: 0;
&:focus-visible {
@@ -106,7 +106,7 @@ export default [
&:not(:active):hover:not(:disabled) {
color: var(
- --hovered-icon-color,
+ --private-hovered-icon-color,
var(--glide-core-icon-primary-hover)
);
}
diff --git a/src/icon-button.ts b/src/icon-button.ts
index 57aa5961a..f4b4d9c36 100644
--- a/src/icon-button.ts
+++ b/src/icon-button.ts
@@ -81,7 +81,13 @@ export default class GlideCoreIconButton extends LitElement {
?disabled=${this.disabled}
${ref(this.#buttonElementRef)}
>
-
+
+
+
`;
}
diff --git a/src/icons/checked.ts b/src/icons/checked.ts
index 573b0609c..3d39bfc1f 100644
--- a/src/icons/checked.ts
+++ b/src/icons/checked.ts
@@ -12,8 +12,8 @@ export default html`
fill="none"
viewBox="0 0 24 24"
style=${styleMap({
- height: 'var(--size, 0.875rem)',
- width: 'var(--size, 0.875rem)',
+ height: 'var(--private-size, 0.875rem)',
+ width: 'var(--private-size, 0.875rem)',
})}
>
-
+
+
+
${when(
@@ -160,8 +166,8 @@ const icons = {
viewBox="0 0 16 16"
fill="none"
style=${styleMap({
- height: 'var(--size, 1rem)',
- width: 'var(--size, 1rem)',
+ height: 'var(--private-size, 1rem)',
+ width: 'var(--private-size, 1rem)',
})}
>
{
- return `"${type}"`;
- }).join(' | '),
+ summary:
+ 'date | email | number | password | search | tel | text | time | url',
},
},
},
diff --git a/src/input.styles.ts b/src/input.styles.ts
index 35c916caf..487ee202b 100644
--- a/src/input.styles.ts
+++ b/src/input.styles.ts
@@ -60,9 +60,10 @@ export default [
border-color: var(--glide-core-status-error);
}
- /* We had to resort to a class selector because there may be a bug in Chrome and Safari
- * with ":read-only": https://bugs.chromium.org/p/chromium/issues/detail?id=1519649
- */
+ /*
+ We had to resort to a class selector because there may be a bug in Chrome and Safari
+ with ":read-only": https://bugs.chromium.org/p/chromium/issues/detail?id=1519649
+ */
&.readonly {
background-color: transparent;
border: 1px solid transparent;
diff --git a/src/input.ts b/src/input.ts
index a046b4691..822099597 100644
--- a/src/input.ts
+++ b/src/input.ts
@@ -24,25 +24,6 @@ declare global {
}
}
-/*
- * A selection of `type` attributes that align with native that we support
- * with our component.
- * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
- */
-export const SUPPORTED_TYPES = [
- 'date',
- 'email',
- 'number',
- 'password',
- 'search',
- 'tel',
- 'text',
- 'time',
- 'url',
-] as const;
-
-type SupportedTypes = (typeof SUPPORTED_TYPES)[number];
-
/**
* @attribute {Boolean} hide-label
* @attribute {Boolean} password-toggle
@@ -68,8 +49,17 @@ export default class GlideCoreInput extends LitElement implements FormControl {
static override styles = styles;
- @property()
- type: SupportedTypes = 'text';
+ @property({ reflect: true })
+ type:
+ | 'date'
+ | 'email'
+ | 'number'
+ | 'password'
+ | 'search'
+ | 'tel'
+ | 'text'
+ | 'time'
+ | 'url' = 'text';
@property({ reflect: true })
name = '';
@@ -145,11 +135,11 @@ export default class GlideCoreInput extends LitElement implements FormControl {
@property({ reflect: true })
tooltip?: string;
- get form() {
+ get form(): HTMLFormElement | null {
return this.#internals.form;
}
- get validity() {
+ get validity(): ValidityState {
if (this.pattern) {
// A validation message is required but unused because we disable native validation feedback.
// And an empty string isn't allowed. Thus a single space.
@@ -201,7 +191,7 @@ export default class GlideCoreInput extends LitElement implements FormControl {
return this.#internals.validity;
}
- checkValidity() {
+ checkValidity(): boolean {
this.isCheckingValidity = true;
const isValid = this.#internals.checkValidity();
this.isCheckingValidity = false;
@@ -214,11 +204,11 @@ export default class GlideCoreInput extends LitElement implements FormControl {
this.form?.removeEventListener('formdata', this.#onFormdata);
}
- formAssociatedCallback() {
+ formAssociatedCallback(): void {
this.form?.addEventListener('formdata', this.#onFormdata);
}
- formResetCallback() {
+ formResetCallback(): void {
this.value = this.getAttribute('value') ?? '';
}
@@ -254,7 +244,12 @@ export default class GlideCoreInput extends LitElement implements FormControl {
})}
slot="control"
>
-
+
+
+
${this.type === 'search'
? magnifyingGlassIcon
- : html``}
+ : html`
+
+
+
+ `}
@@ -332,7 +334,12 @@ export default class GlideCoreInput extends LitElement implements FormControl {
),
})}
name="description"
- >
+ >
+
+
${when(
this.#isShowValidationFeedback && this.validityMessage,
@@ -378,7 +385,7 @@ export default class GlideCoreInput extends LitElement implements FormControl {
`;
}
- reportValidity() {
+ reportValidity(): boolean {
this.isReportValidityOrSubmit = true;
const isValid = this.#internals.reportValidity();
@@ -389,11 +396,11 @@ export default class GlideCoreInput extends LitElement implements FormControl {
return isValid;
}
- resetValidityFeedback() {
+ resetValidityFeedback(): void {
this.isReportValidityOrSubmit = false;
}
- setCustomValidity(message: string) {
+ setCustomValidity(message: string): void {
this.validityMessage = message;
if (message === '') {
@@ -417,7 +424,7 @@ export default class GlideCoreInput extends LitElement implements FormControl {
}
}
- setValidity(flags?: ValidityStateFlags, message?: string) {
+ setValidity(flags?: ValidityStateFlags, message?: string): void {
this.validityMessage = message;
// A validation message is required but unused because we disable native validation feedback.
@@ -539,16 +546,14 @@ export default class GlideCoreInput extends LitElement implements FormControl {
this.hasFocus = false;
}
- #onInputChange(event: Event) {
+ #onInputChange() {
if (this.#inputElementRef.value?.value) {
this.value = this.#inputElementRef.value?.value;
}
// Unlike "input" events, "change" events aren't composed. So we have to
// manually dispatch them.
- this.dispatchEvent(
- new Event(event.type, { bubbles: true, composed: true }),
- );
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
}
#onInputFocus() {
diff --git a/src/label.styles.ts b/src/label.styles.ts
index ab317c7b1..fa7bcff9b 100644
--- a/src/label.styles.ts
+++ b/src/label.styles.ts
@@ -10,7 +10,7 @@ export default [
css`
.component {
&.horizontal {
- --column-gap: var(--glide-core-spacing-sm);
+ --private-column-gap: var(--glide-core-spacing-sm);
column-gap: var(--glide-core-spacing-sm);
display: grid;
@@ -27,8 +27,8 @@ export default [
}
&.middle {
- grid-template-columns: calc(50% - var(--column-gap) / 2) calc(
- 50% - var(--column-gap) / 2
+ grid-template-columns: calc(50% - var(--private-column-gap) / 2) calc(
+ 50% - var(--private-column-gap) / 2
);
}
@@ -67,9 +67,9 @@ export default [
&.visible {
/*
- The default is "display: content". But "order" does not work with
- "display: content" and "order" is needed above.
- */
+ The default is "display: content". But "order" does not work with
+ "display: content" and "order" is needed above.
+ */
display: block;
}
}
@@ -81,11 +81,10 @@ export default [
color: var(--glide-core-text-body-1);
/*
- Any "display" that's not inline-level will do. We don't want the button to
- acquire a line box, which will make it taller than its content and thus
- make it difficult to center vertically with the label.
- */
-
+ Any "display" that's not inline-level will do. We don't want the button to
+ acquire a line box, which will make it taller than its content and thus
+ make it difficult to center vertically with the label.
+ */
display: flex;
padding: 0;
diff --git a/src/label.ts b/src/label.ts
index 99baa8789..ae4cefcd0 100644
--- a/src/label.ts
+++ b/src/label.ts
@@ -138,7 +138,9 @@ export default class GlideCoreLabel extends LitElement {
@slotchange=${this.#onDefaultSlotChange}
${assertSlot()}
${ref(this.#defaultSlotElementRef)}
- >
+ >
+
+
${this.required
? html`*`
@@ -159,7 +161,12 @@ export default class GlideCoreLabel extends LitElement {
})}
name="control"
${assertSlot()}
- >
+ >
+
+
+ >
+
+
+ >
+
+
`;
}
diff --git a/src/library/get-parent-class-name.ts b/src/library/get-parent-class-name.ts
new file mode 100644
index 000000000..99eb5e7bf
--- /dev/null
+++ b/src/library/get-parent-class-name.ts
@@ -0,0 +1,17 @@
+import { isClassDeclaration, isIdentifier, type Node } from 'typescript';
+
+export default (node: Node) => {
+ if (isClassDeclaration(node) && node.name && isIdentifier(node.name)) {
+ return node.name.text;
+ }
+
+ let parent = node.parent;
+
+ while (parent && !isClassDeclaration(parent)) {
+ parent = parent.parent;
+ }
+
+ if (parent?.name && isIdentifier(parent.name)) {
+ return parent.name.text;
+ }
+};
diff --git a/src/menu.button.styles.ts b/src/menu.button.styles.ts
index 27cf0fd4d..d3cb5025d 100644
--- a/src/menu.button.styles.ts
+++ b/src/menu.button.styles.ts
@@ -9,10 +9,10 @@ export default [
border-radius: var(--glide-core-spacing-sm);
display: flex;
font: inherit;
- gap: var(--gap);
+ gap: var(--private-gap);
inline-size: 100%;
- padding-block: var(--padding-block);
- padding-inline: var(--padding-inline);
+ padding-block: var(--private-padding-block);
+ padding-inline: var(--private-padding-inline);
transition: background-color 100ms ease-in-out;
user-select: none;
diff --git a/src/menu.button.ts b/src/menu.button.ts
index 5db9c0914..2ce20a2c7 100644
--- a/src/menu.button.ts
+++ b/src/menu.button.ts
@@ -28,8 +28,11 @@ export default class GlideCoreMenuButton extends LitElement {
static override styles = styles;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get disabled() {
+ get disabled(): boolean {
return this.#isDisabled;
}
@@ -83,7 +86,10 @@ export default class GlideCoreMenuButton extends LitElement {
type="button"
${ref(this.#componentElementRef)}
>
-
+
+
+
+
${this.label}
`;
}
diff --git a/src/menu.link.styles.ts b/src/menu.link.styles.ts
index 89d98e350..0ed210a06 100644
--- a/src/menu.link.styles.ts
+++ b/src/menu.link.styles.ts
@@ -10,10 +10,10 @@ export default [
box-sizing: border-box;
display: flex;
font: inherit;
- gap: var(--gap);
+ gap: var(--private-gap);
inline-size: 100%;
- padding-block: var(--padding-block);
- padding-inline: var(--padding-inline);
+ padding-block: var(--private-padding-block);
+ padding-inline: var(--private-padding-inline);
text-decoration: none;
transition: background-color 100ms ease-in-out;
user-select: none;
diff --git a/src/menu.link.ts b/src/menu.link.ts
index 8686a89fb..3ac85c3a7 100644
--- a/src/menu.link.ts
+++ b/src/menu.link.ts
@@ -29,8 +29,11 @@ export default class GlideCoreMenuLink extends LitElement {
static override styles = styles;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get disabled() {
+ get disabled(): boolean {
return this.#isDisabled;
}
@@ -97,7 +100,10 @@ export default class GlideCoreMenuLink extends LitElement {
@click=${this.#onClick}
${ref(this.#componentElementRef)}
>
-
+
+
+
+
${this.label}
`;
}
diff --git a/src/menu.options.styles.ts b/src/menu.options.styles.ts
index 495f6b85e..da6d5bb2c 100644
--- a/src/menu.options.styles.ts
+++ b/src/menu.options.styles.ts
@@ -9,9 +9,9 @@ export default [
.component {
&.large {
- --gap: var(--glide-core-spacing-sm);
- --padding-inline: var(--glide-core-spacing-sm);
- --padding-block: var(--glide-core-spacing-xxs);
+ --private-gap: var(--glide-core-spacing-sm);
+ --private-padding-inline: var(--glide-core-spacing-sm);
+ --private-padding-block: var(--glide-core-spacing-xxs);
font-family: var(--glide-core-body-sm-font-family);
font-size: var(--glide-core-body-sm-font-size);
@@ -21,10 +21,10 @@ export default [
}
&.small {
- --gap: var(--glide-core-spacing-xs);
- --padding-inline: var(--glide-core-spacing-xs);
- --padding-block: var(--glide-core-spacing-xxxs);
- --size: 0.75rem;
+ --private-gap: var(--glide-core-spacing-xs);
+ --private-padding-inline: var(--glide-core-spacing-xs);
+ --private-padding-block: var(--glide-core-spacing-xxxs);
+ --private-size: 0.75rem;
font-family: var(--glide-core-body-xs-font-family);
font-size: var(--glide-core-body-xs-font-size);
diff --git a/src/menu.options.ts b/src/menu.options.ts
index 25ed82f5d..d06634f4a 100644
--- a/src/menu.options.ts
+++ b/src/menu.options.ts
@@ -80,7 +80,9 @@ export default class GlideCoreMenuOptions extends LitElement {
+ >
+
+
`;
}
diff --git a/src/menu.stories.ts b/src/menu.stories.ts
index b10e5b42e..2c3d0c842 100644
--- a/src/menu.stories.ts
+++ b/src/menu.stories.ts
@@ -71,7 +71,7 @@ const meta: Meta = {
type: {
summary: 'Element',
detail:
- 'The element to which the menu will anchor, which can be any focusable element',
+ 'The element to which the menu will anchor. Can be any focusable element',
},
},
type: { name: 'function', required: true },
diff --git a/src/menu.ts b/src/menu.ts
index 4b3a809fa..5f40d59c8 100644
--- a/src/menu.ts
+++ b/src/menu.ts
@@ -1,11 +1,5 @@
import { html, LitElement } from 'lit';
-import {
- autoUpdate,
- computePosition,
- flip,
- offset,
- type Placement,
-} from '@floating-ui/dom';
+import { autoUpdate, computePosition, flip, offset } from '@floating-ui/dom';
import { createRef, ref } from 'lit/directives/ref.js';
import { customElement, property } from 'lit/decorators.js';
import { nanoid } from 'nanoid';
@@ -40,8 +34,11 @@ export default class GlideCoreMenu extends LitElement {
static override styles = styles;
+ /**
+ * @default 4
+ */
@property({ reflect: true, type: Number })
- get offset() {
+ get offset(): number {
return (
this.#offset ??
Number.parseFloat(
@@ -59,8 +56,11 @@ export default class GlideCoreMenu extends LitElement {
this.#offset = offset;
}
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get open() {
+ get open(): boolean {
return this.#isOpen;
}
@@ -84,10 +84,25 @@ export default class GlideCoreMenu extends LitElement {
}
@property({ reflect: true })
- placement: Placement = 'bottom-start';
-
+ placement:
+ | 'bottom'
+ | 'left'
+ | 'right'
+ | 'top'
+ | 'bottom-start'
+ | 'bottom-end'
+ | 'left-start'
+ | 'left-end'
+ | 'right-start'
+ | 'right-end'
+ | 'top-start'
+ | 'top-end' = 'bottom-start';
+
+ /**
+ * @default 'large'
+ */
@property({ reflect: true })
- get size() {
+ get size(): 'small' | 'large' {
return this.#size;
}
@@ -203,7 +218,12 @@ export default class GlideCoreMenu extends LitElement {
@slotchange=${this.#onTargetSlotChange}
${assertSlot([Element])}
${ref(this.#targetSlotElementRef)}
- >
+ >
+
+
+ >
+
+
`;
}
diff --git a/src/modal.icon-button.styles.ts b/src/modal.icon-button.styles.ts
index d8d527ade..6f0991028 100644
--- a/src/modal.icon-button.styles.ts
+++ b/src/modal.icon-button.styles.ts
@@ -3,7 +3,7 @@ import { css } from 'lit';
export default [
css`
::slotted(*) {
- --size: 1.125rem;
+ --private-size: 1.125rem;
block-size: 1.125rem;
inline-size: 1.125rem;
diff --git a/src/modal.icon-button.ts b/src/modal.icon-button.ts
index fed76a87d..8c7121eef 100644
--- a/src/modal.icon-button.ts
+++ b/src/modal.icon-button.ts
@@ -39,7 +39,13 @@ export default class GlideCoreModalIconButton extends LitElement {
override render() {
return html`
-
+
+
+
`;
}
diff --git a/src/modal.styles.ts b/src/modal.styles.ts
index 22b688880..bf486b8eb 100644
--- a/src/modal.styles.ts
+++ b/src/modal.styles.ts
@@ -61,7 +61,7 @@ export default [
}
::slotted([slot='tertiary']) {
- --size: 1rem;
+ --private-size: 1rem;
display: contents;
size: 1rem;
@@ -127,7 +127,7 @@ export default [
}
.severity {
- --size: 1.5rem;
+ --private-size: 1.5rem;
display: flex;
@@ -153,7 +153,7 @@ export default [
}
.close-button {
- --size: 1.25rem;
+ --private-size: 1.25rem;
/*
Flex so the icon doesn't sit on the baseline and extend the height of
diff --git a/src/modal.ts b/src/modal.ts
index ebf49f3d3..ebb58e796 100644
--- a/src/modal.ts
+++ b/src/modal.ts
@@ -62,8 +62,11 @@ export default class GlideCoreModal extends LitElement {
@required
label?: string;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get open() {
+ get open(): boolean {
return this.#isOpen;
}
@@ -232,7 +235,9 @@ export default class GlideCoreModal extends LitElement {
name="header-actions"
${assertSlot([GlideCoreModalIconButton], true)}
${ref(this.#headerActionsSlotElementRef)}
- >
+ >
+
+
-
+
+
+
diff --git a/src/popover.stories.ts b/src/popover.stories.ts
index affb775df..d6d11e48b 100644
--- a/src/popover.stories.ts
+++ b/src/popover.stories.ts
@@ -81,7 +81,7 @@ const meta: Meta = {
type: {
summary: 'Element',
detail:
- '// The element to which the popover will anchor, which can be any focusable element',
+ '// The element to which the popover will anchor. Can be any focusable element',
},
},
type: { name: 'function', required: true },
diff --git a/src/popover.styles.ts b/src/popover.styles.ts
index 1a0b71008..e3933cd2c 100644
--- a/src/popover.styles.ts
+++ b/src/popover.styles.ts
@@ -75,8 +75,8 @@ export default [
}
.arrow {
- --arrow-height: 0.5625rem;
- --arrow-width: 1rem;
+ --private-arrow-height: 0.5625rem;
+ --private-arrow-width: 1rem;
color: var(--glide-core-surface-modal);
display: flex;
@@ -84,14 +84,14 @@ export default [
&.top,
&.bottom {
- block-size: var(--arrow-height);
- inline-size: var(--arrow-width);
+ block-size: var(--private-arrow-height);
+ inline-size: var(--private-arrow-width);
}
&.right,
&.left {
- block-size: var(--arrow-width);
- inline-size: var(--arrow-height);
+ block-size: var(--private-arrow-width);
+ inline-size: var(--private-arrow-height);
order: 2;
}
}
diff --git a/src/popover.ts b/src/popover.ts
index f0f8a358a..f8becc678 100644
--- a/src/popover.ts
+++ b/src/popover.ts
@@ -41,8 +41,11 @@ export default class GlideCorePopover extends LitElement {
static override styles = styles;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get disabled() {
+ get disabled(): boolean {
return this.#isDisabled;
}
@@ -56,8 +59,11 @@ export default class GlideCorePopover extends LitElement {
}
}
+ /**
+ * @default 4
+ */
@property({ reflect: true, type: Number })
- get offset() {
+ get offset(): number {
return (
this.#offset ??
Number.parseFloat(
@@ -75,8 +81,11 @@ export default class GlideCorePopover extends LitElement {
this.#offset = offset;
}
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get open() {
+ get open(): boolean {
return this.#isOpen;
}
@@ -101,7 +110,7 @@ export default class GlideCorePopover extends LitElement {
/*
The placement of the popover relative to its target. Automatic placement will
- take over if the popover is cut off by the viewport. "bottom" by default.
+ take over if the popover is cut off by the viewport.
*/
@property()
placement?: 'bottom' | 'left' | 'right' | 'top';
@@ -191,7 +200,12 @@ export default class GlideCorePopover extends LitElement {
@keydown=${this.#onTargetSlotKeydown}
${assertSlot([Element])}
${ref(this.#targetSlotElementRef)}
- >
+ >
+
+
+ >
+
+
`;
diff --git a/src/radio-group.radio.ts b/src/radio-group.radio.ts
index 9ca75c0e6..8f1e979fa 100644
--- a/src/radio-group.radio.ts
+++ b/src/radio-group.radio.ts
@@ -23,16 +23,27 @@ export default class GlideCoreRadioGroupRadio extends LitElement {
static override styles = styles;
+ /**
+ * @default false
+ */
@property({ type: Boolean, reflect: true })
- get checked() {
+ get checked(): boolean {
return this.#checked;
}
- set checked(checked: boolean) {
- const old = this.#checked;
- this.#checked = checked;
+ set checked(isChecked: boolean) {
+ const wasChecked = this.#checked;
- this.ariaChecked = checked.toString();
+ this.#checked = isChecked;
+ this.ariaChecked = isChecked.toString();
+
+ if (isChecked && wasChecked !== isChecked) {
+ this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
+
+ this.dispatchEvent(
+ new Event('change', { bubbles: true, composed: true }),
+ );
+ }
// `this.checked` can be set programmatically. Radio Group needs to know when
// that happens so it can update its own `this.value`.
@@ -42,15 +53,18 @@ export default class GlideCoreRadioGroupRadio extends LitElement {
detail: {
// Without knowing what the old value was, Radio Group would be unable to
// update `this.value`.
- old,
- new: checked,
+ old: wasChecked,
+ new: isChecked,
},
}),
);
}
+ /**
+ * @default false
+ */
@property({ type: Boolean, reflect: true })
- get disabled() {
+ get disabled(): boolean {
return this.#disabled;
}
@@ -79,6 +93,9 @@ export default class GlideCoreRadioGroupRadio extends LitElement {
this.ariaInvalid = invalid.toString();
}
+ /**
+ * @default undefined
+ */
@property({ reflect: true })
@required
get label(): string | undefined {
@@ -102,8 +119,11 @@ export default class GlideCoreRadioGroupRadio extends LitElement {
this.ariaRequired = required.toString();
}
+ /**
+ * @default undefined
+ */
@property()
- get value() {
+ get value(): string {
return this.#value;
}
diff --git a/src/radio-group.ts b/src/radio-group.ts
index fe52801ab..a2caab301 100644
--- a/src/radio-group.ts
+++ b/src/radio-group.ts
@@ -47,8 +47,11 @@ export default class GlideCoreRadioGroup
static override styles = styles;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get disabled() {
+ get disabled(): boolean {
return this.#isDisabled;
}
@@ -81,8 +84,11 @@ export default class GlideCoreRadioGroup
@property({ reflect: true })
tooltip?: string;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get required() {
+ get required(): boolean {
return this.#isRequired;
}
@@ -99,8 +105,11 @@ export default class GlideCoreRadioGroup
}
// Intentionally not reflected to match native.
+ /**
+ * @default ''
+ */
@property()
- get value() {
+ get value(): string {
return this.#value;
}
@@ -134,7 +143,7 @@ export default class GlideCoreRadioGroup
@property({ reflect: true })
readonly version = packageJson.version;
- checkValidity() {
+ checkValidity(): boolean {
this.isCheckingValidity = true;
const isValid = this.#internals.checkValidity();
this.isCheckingValidity = false;
@@ -205,11 +214,11 @@ export default class GlideCoreRadioGroup
radio?.focus(options);
}
- get form() {
+ get form(): HTMLFormElement | null {
return this.#internals.form;
}
- get validity() {
+ get validity(): ValidityState {
const isChecked = this.#radioElements.some(({ checked }) => checked);
if (this.required && !isChecked && !this.disabled) {
@@ -248,11 +257,11 @@ export default class GlideCoreRadioGroup
return this.#internals.validity;
}
- formAssociatedCallback() {
+ formAssociatedCallback(): void {
this.form?.addEventListener('formdata', this.#onFormdata);
}
- formResetCallback() {
+ formResetCallback(): void {
this.value = this.getAttribute('value') ?? '';
}
@@ -293,7 +302,9 @@ export default class GlideCoreRadioGroup
@private-value-change=${this.#onRadiosValueChange}
${assertSlot([GlideCoreRadioGroupRadio])}
${ref(this.#defaultSlotElementRef)}
- >
+ >
+
+
@@ -305,7 +316,12 @@ export default class GlideCoreRadioGroup
),
})}
name="description"
- >
+ >
+
+
${when(
this.#isShowValidationFeedback && this.validityMessage,
@@ -320,7 +336,7 @@ export default class GlideCoreRadioGroup
`;
}
- reportValidity() {
+ reportValidity(): boolean {
this.isReportValidityOrSubmit = true;
const isValid = this.#internals.reportValidity();
@@ -331,11 +347,11 @@ export default class GlideCoreRadioGroup
return isValid;
}
- resetValidityFeedback() {
+ resetValidityFeedback(): void {
this.isReportValidityOrSubmit = false;
}
- setCustomValidity(message: string) {
+ setCustomValidity(message: string): void {
this.validityMessage = message;
if (message === '') {
@@ -356,7 +372,7 @@ export default class GlideCoreRadioGroup
}
}
- setValidity(flags?: ValidityStateFlags, message?: string) {
+ setValidity(flags?: ValidityStateFlags, message?: string): void {
this.validityMessage = message;
this.#internals.setValidity(flags, ' ', this.#componentElementRef.value);
}
@@ -444,8 +460,6 @@ export default class GlideCoreRadioGroup
radio.checked = true;
radio.tabIndex = 0;
radio.focus();
- radio.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
- radio.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
}
#onComponentClick(event: MouseEvent) {
diff --git a/src/split-button.primary-button.ts b/src/split-button.primary-button.ts
index d8a3b87b5..18a26141f 100644
--- a/src/split-button.primary-button.ts
+++ b/src/split-button.primary-button.ts
@@ -53,7 +53,7 @@ export default class GlideCoreSplitButtonPrimaryButton extends LitElement {
label?: string;
@property()
- privateSize: 'large' | 'small' = 'large';
+ privateSize: 'small' | 'large' = 'large';
@property()
privateVariant: 'primary' | 'secondary' = 'primary';
@@ -76,7 +76,13 @@ export default class GlideCoreSplitButtonPrimaryButton extends LitElement {
type="button"
?disabled=${this.disabled}
>
-
+
+
+
+
${this.label}
`;
}
diff --git a/src/split-button.primary-link.ts b/src/split-button.primary-link.ts
index e4e03a0b6..70aaa0323 100644
--- a/src/split-button.primary-link.ts
+++ b/src/split-button.primary-link.ts
@@ -40,7 +40,7 @@ export default class GlideCoreSplitButtonPrimaryLink extends LitElement {
url?: string;
@property()
- privateSize: 'large' | 'small' = 'large';
+ privateSize: 'small' | 'large' = 'large';
@property()
privateVariant: 'primary' | 'secondary' = 'primary';
@@ -60,7 +60,13 @@ export default class GlideCoreSplitButtonPrimaryLink extends LitElement {
})}
role="link"
>
-
+
+
+
+
${this.label}
`;
}
@@ -74,7 +80,10 @@ export default class GlideCoreSplitButtonPrimaryLink extends LitElement {
data-test="component"
href=${ifDefined(this.url)}
>
-
+
+
+
+
${this.label}
`;
}
diff --git a/src/split-button.secondary-button.ts b/src/split-button.secondary-button.ts
index 0a5786767..775c6745e 100644
--- a/src/split-button.secondary-button.ts
+++ b/src/split-button.secondary-button.ts
@@ -55,7 +55,7 @@ export default class GlideCoreSplitButtonSecondaryButton extends LitElement {
privateActive = false;
@property()
- privateSize: 'large' | 'small' = 'large';
+ privateSize: 'small' | 'large' = 'large';
@property()
privateVariant: 'primary' | 'secondary' = 'primary';
@@ -115,7 +115,9 @@ export default class GlideCoreSplitButtonSecondaryButton extends LitElement {
-
+
+
+
`;
diff --git a/src/split-button.ts b/src/split-button.ts
index 3b2f805ce..797044096 100644
--- a/src/split-button.ts
+++ b/src/split-button.ts
@@ -32,12 +32,15 @@ export default class GlideCoreSplitButton extends LitElement {
static override styles = styles;
+ /**
+ * @default 'large'
+ */
@property({ reflect: true })
- get size() {
+ get size(): 'small' | 'large' {
return this.#size;
}
- set size(size: 'large' | 'small') {
+ set size(size: 'small' | 'large') {
this.#size = size;
if (this.primaryButtonElement) {
@@ -49,8 +52,11 @@ export default class GlideCoreSplitButton extends LitElement {
}
}
+ /**
+ * @default 'primary'
+ */
@property({ reflect: true })
- get variant() {
+ get variant(): 'primary' | 'secondary' {
return this.#variant;
}
@@ -100,14 +106,18 @@ export default class GlideCoreSplitButton extends LitElement {
GlideCoreSplitButtonPrimaryLink,
])}
${ref(this.#defaultSlotElementRef)}
- >
+ >
+
+
+ >
+
+
`;
}
@@ -116,7 +126,7 @@ export default class GlideCoreSplitButton extends LitElement {
#secondaryButtonSlotElementRef = createRef();
- #size: 'large' | 'small' = 'large';
+ #size: 'small' | 'large' = 'large';
#variant: 'primary' | 'secondary' = 'primary';
diff --git a/src/stylelint/plugin.ts b/src/stylelint/plugin.ts
new file mode 100644
index 000000000..c11c2e1c4
--- /dev/null
+++ b/src/stylelint/plugin.ts
@@ -0,0 +1,9 @@
+import stylelint from 'stylelint';
+import noUnprefixedPrivateCustomPropertyName from './rules/no-unprefixed-private-custom-property.js';
+
+export default [
+ stylelint.createPlugin(
+ noUnprefixedPrivateCustomPropertyName.ruleName,
+ noUnprefixedPrivateCustomPropertyName,
+ ),
+];
diff --git a/src/stylelint/rules/no-unprefixed-private-custom-property.test.ts b/src/stylelint/rules/no-unprefixed-private-custom-property.test.ts
new file mode 100644
index 000000000..e21ea4368
--- /dev/null
+++ b/src/stylelint/rules/no-unprefixed-private-custom-property.test.ts
@@ -0,0 +1,73 @@
+import stylelint from 'stylelint';
+import { expect } from 'vitest';
+
+const config = {
+ plugins: ['../plugin.js'],
+ rules: {
+ 'glide-core/no-unprefixed-private-custom-property': true,
+ },
+};
+
+it('warns if a custom property not on the host is unprefixed', async () => {
+ const {
+ results: [{ warnings }],
+ } = await stylelint.lint({
+ code: `
+ .component {
+ --color: black;
+ }
+ `,
+ cwd: import.meta.dirname.replace('src', 'dist'),
+ config,
+ });
+
+ expect(warnings).toHaveLength(1);
+});
+
+it('does not warn if a custom property not on the host is prefixed', async () => {
+ const {
+ results: [{ warnings }],
+ } = await stylelint.lint({
+ code: `
+ .component {
+ --private-color: black;
+ }
+ `,
+ cwd: import.meta.dirname.replace('src', 'dist'),
+ config,
+ });
+
+ expect(warnings).toHaveLength(0);
+});
+
+it('warns if a custom property on the host is prefixed', async () => {
+ const {
+ results: [{ warnings }],
+ } = await stylelint.lint({
+ code: `
+ :host {
+ --private-color: black;
+ }
+ `,
+ cwd: import.meta.dirname.replace('src', 'dist'),
+ config,
+ });
+
+ expect(warnings).toHaveLength(1);
+});
+
+it('does not warn if a custom property on the host is unprefixed', async () => {
+ const {
+ results: [{ warnings }],
+ } = await stylelint.lint({
+ code: `
+ :host {
+ --color: black;
+ }
+ `,
+ cwd: import.meta.dirname.replace('src', 'dist'),
+ config,
+ });
+
+ expect(warnings).toHaveLength(0);
+});
diff --git a/src/stylelint/rules/no-unprefixed-private-custom-property.ts b/src/stylelint/rules/no-unprefixed-private-custom-property.ts
new file mode 100644
index 000000000..da30e273b
--- /dev/null
+++ b/src/stylelint/rules/no-unprefixed-private-custom-property.ts
@@ -0,0 +1,70 @@
+import stylelint, { type PostcssResult } from 'stylelint';
+import { Declaration, type Root } from 'postcss';
+
+const ruleName = 'glide-core/no-unprefixed-private-custom-property';
+
+const messages = stylelint.utils.ruleMessages(ruleName, {
+ rejected(selector) {
+ return `Unprefixed custom property in \`${selector}\`. If the custom property is meant to be public, move it inside a \`:host\` selector. Otherwise, prefix it with "private".`;
+ },
+});
+
+function rule(actualOptions: unknown) {
+ return (root: Root, result: PostcssResult) => {
+ const validOptions = stylelint.utils.validateOptions(result, ruleName, {
+ actual: actualOptions,
+ possible: [true],
+ });
+
+ if (!validOptions) {
+ return;
+ }
+
+ root.walkRules((rule) => {
+ if (rule.selector.startsWith(':host')) {
+ for (const node of rule.nodes) {
+ if (
+ node instanceof Declaration &&
+ node.prop.startsWith('--') &&
+ node.prop.startsWith('--private')
+ ) {
+ stylelint.utils.report({
+ result,
+ ruleName,
+ message: messages.rejected(rule.selector),
+ node,
+ word: rule.selector,
+ });
+ }
+ }
+ }
+
+ if (!rule.selector.startsWith(':host')) {
+ for (const node of rule.nodes) {
+ if (
+ node instanceof Declaration &&
+ node.prop.startsWith('--') &&
+ !node.prop.startsWith('--private')
+ ) {
+ stylelint.utils.report({
+ result,
+ ruleName,
+ message: messages.rejected(rule.selector),
+ node,
+ word: rule.selector,
+ });
+ }
+ }
+ }
+ });
+ };
+}
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+
+rule.meta = {
+ url: `https://github.com/CrowdStrike/glide-core/blob/main/src/stylelint/rules/${ruleName}.ts`,
+};
+
+export default rule;
diff --git a/src/tab.group.styles.ts b/src/tab.group.styles.ts
index c1aedc770..18c8f028f 100644
--- a/src/tab.group.styles.ts
+++ b/src/tab.group.styles.ts
@@ -3,6 +3,11 @@ import { css } from 'lit';
export default [
css`
:host {
+ --tabs-padding-block-end: 0rem;
+ --tabs-padding-block-start: 0rem;
+ --tabs-padding-inline-end: 0rem;
+ --tabs-padding-inline-start: 0rem;
+
background-color: transparent;
block-size: 100%;
display: flex;
@@ -39,12 +44,12 @@ export default [
background: var(--glide-core-border-focus);
block-size: 0.125rem;
content: '';
- inline-size: var(--selected-tab-indicator-width);
+ inline-size: var(--private-selected-tab-indicator-width);
inset-block-end: 0;
inset-inline: 0;
position: absolute;
transform-origin: left;
- translate: var(--selected-tab-indicator-translate, 0) 0;
+ translate: var(--private-selected-tab-indicator-translate, 0) 0;
}
&.animated {
@@ -59,7 +64,7 @@ export default [
}
.overflow-button {
- --size: 1.125rem;
+ --private-size: 1.125rem;
align-items: center;
background-color: transparent;
diff --git a/src/tab.group.ts b/src/tab.group.ts
index d8eaf6d72..4cf4616f0 100644
--- a/src/tab.group.ts
+++ b/src/tab.group.ts
@@ -113,7 +113,9 @@ export default class GlideCoreTabGroup extends LitElement {
name="nav"
@slotchange=${this.#onNavSlotChange}
${assertSlot([GlideCoreTab])}
- >
+ >
+
+
${when(
@@ -138,7 +140,10 @@ export default class GlideCoreTabGroup extends LitElement {
`,
)}
-
+
+
+
+
`;
}
@@ -389,7 +394,7 @@ export default class GlideCoreTabGroup extends LitElement {
: this.selectedTab.offsetLeft - this.#tabElements[0].offsetLeft;
this.#componentElementRef.value.style.setProperty(
- '--selected-tab-indicator-translate',
+ '--private-selected-tab-indicator-translate',
`${selectedTabIndicatorTranslateLeft}px`,
);
@@ -403,7 +408,7 @@ export default class GlideCoreTabGroup extends LitElement {
this.selectedTab.getBoundingClientRect();
this.#componentElementRef.value.style.setProperty(
- '--selected-tab-indicator-width',
+ '--private-selected-tab-indicator-width',
`${selectedTabWidth - selectedTabIndicatorWidthAdjustment}px`,
);
@@ -434,12 +439,5 @@ export default class GlideCoreTabGroup extends LitElement {
#showTab(tab: GlideCoreTab) {
this.selectedTab = tab;
this.#setSelectedTab();
-
- tab.dispatchEvent(
- new Event('selected', {
- bubbles: true,
- composed: true,
- }),
- );
}
}
diff --git a/src/tab.panel.styles.ts b/src/tab.panel.styles.ts
index f1b41e16f..5318b7a74 100644
--- a/src/tab.panel.styles.ts
+++ b/src/tab.panel.styles.ts
@@ -8,6 +8,13 @@ export default [
${visuallyHidden('.hidden')}
`,
css`
+ :host {
+ --padding-inline-end: 0rem;
+ --padding-inline-start: 0rem;
+ }
+
+ /* https://github.com/csstools/stylelint-use-nesting/issues/23 */
+ /* stylelint-disable-next-line csstools/use-nesting */
:host(:focus-visible) {
outline: none;
}
@@ -38,8 +45,8 @@ export default [
can set these properties on the parent Tab Group
and it will apply to all child panels for convenience.
*/
- padding-inline-end: var(--panel-padding-inline-end);
- padding-inline-start: var(--panel-padding-inline-start);
+ padding-inline-end: var(--padding-inline-end);
+ padding-inline-start: var(--padding-inline-start);
&.selected {
block-size: 100%;
diff --git a/src/tab.panel.ts b/src/tab.panel.ts
index 98642c486..da8510821 100644
--- a/src/tab.panel.ts
+++ b/src/tab.panel.ts
@@ -20,8 +20,6 @@ declare global {
@customElement('glide-core-tab-panel')
@final
export default class GlideCoreTabPanel extends LitElement {
- static instanceCount = 0;
-
static override shadowRootOptions: ShadowRootInit = {
...LitElement.shadowRootOptions,
mode: shadowRootMode,
@@ -30,15 +28,15 @@ export default class GlideCoreTabPanel extends LitElement {
static override styles = styles;
/**
- * The name of this panel
- * The corresponding will have a `panel` attribute with this name
+ * The corresponding GlideCoreTab should have a `panel` attribute with this name.
*/
@property({ reflect: true })
@required
name?: string;
// Private because it's only meant to be used by Tab Group.
- @property({ type: Boolean }) get privateIsSelected() {
+ @property({ type: Boolean })
+ get privateIsSelected() {
return this.#isSelected;
}
@@ -64,7 +62,12 @@ export default class GlideCoreTabPanel extends LitElement {
})}
data-test="tab-panel"
>
-
+
+
+
`;
}
diff --git a/src/tab.ts b/src/tab.ts
index e16da3177..cdef39dd1 100644
--- a/src/tab.ts
+++ b/src/tab.ts
@@ -36,7 +36,27 @@ export default class GlideCoreTab extends LitElement {
@property({ type: Boolean, reflect: true }) disabled = false;
- @property({ type: Boolean, reflect: true }) selected = false;
+ /**
+ * @default false
+ */
+ @property({ type: Boolean, reflect: true })
+ get selected(): boolean {
+ return this.#isSelected;
+ }
+
+ set selected(isSelected: boolean) {
+ const hasChanged = isSelected !== this.#isSelected;
+ this.#isSelected = isSelected;
+
+ if (isSelected && hasChanged) {
+ this.dispatchEvent(
+ new Event('selected', {
+ bubbles: true,
+ composed: true,
+ }),
+ );
+ }
+ }
@property({ reflect: true })
readonly version = packageJson.version;
@@ -54,8 +74,18 @@ export default class GlideCoreTab extends LitElement {
})}
>
-
-
+
+
+
+
+
+
+
`;
}
@@ -76,4 +106,6 @@ export default class GlideCoreTab extends LitElement {
}
#id = nanoid();
+
+ #isSelected = false;
}
diff --git a/src/tabs.stories.ts b/src/tabs.stories.ts
index cdf18130b..c6d5210ae 100644
--- a/src/tabs.stories.ts
+++ b/src/tabs.stories.ts
@@ -38,8 +38,6 @@ const meta: Meta = {
'slot="default"': '',
'slot="nav"': '',
version: '',
- '--panel-padding-inline-end': '',
- '--panel-padding-inline-start': '',
'--tabs-padding-block-end': '',
'--tabs-padding-block-start': '',
'--tabs-padding-inline-end': '',
@@ -63,6 +61,8 @@ const meta: Meta = {
'.name': '',
'[slot="default"]': 'Panel',
'.version': '',
+ '[--padding-inline-end]': '',
+ '[--padding-inline-start]': '',
},
play(context) {
const tabs = context.canvasElement.querySelectorAll('glide-core-tab');
@@ -88,10 +88,6 @@ const meta: Meta = {
return html`
-
+ [--padding-inline-end]'] ||
+ null,
+ '--padding-inline-start':
+ arguments_['[--padding-inline-start]'] ||
+ null,
+ })}
+ >
${unsafeHTML(arguments_['[slot="default"]'])}
With Icon
@@ -155,20 +161,6 @@ const meta: Meta = {
type: { summary: 'string', detail: '// For debugging' },
},
},
- '--panel-padding-inline-end': {
- table: {
- type: {
- summary: 'CSS custom property',
- },
- },
- },
- '--panel-padding-inline-start': {
- table: {
- type: {
- summary: 'CSS custom property',
- },
- },
- },
'--tabs-padding-block-end': {
table: {
type: {
@@ -335,6 +327,24 @@ const meta: Meta = {
type: { summary: 'string', detail: '// For debugging' },
},
},
+ '[--padding-inline-end]': {
+ name: '--padding-inline-end',
+ table: {
+ category: 'Tab Panel',
+ type: {
+ summary: 'CSS custom property',
+ },
+ },
+ },
+ '[--padding-inline-start]': {
+ name: '--padding-inline-start',
+ table: {
+ category: 'Tab Panel',
+ type: {
+ summary: 'CSS custom property',
+ },
+ },
+ },
},
};
@@ -420,7 +430,19 @@ export const WithOverflow: StoryObj = {
Ten
- One
+ [--padding-inline-end]'] ||
+ null,
+ '--padding-inline-start':
+ arguments_['[--padding-inline-start]'] ||
+ null,
+ })}
+ >
+ One
+
Two
Three
Four
diff --git a/src/tag.styles.ts b/src/tag.styles.ts
index e40361242..2ce182ee7 100644
--- a/src/tag.styles.ts
+++ b/src/tag.styles.ts
@@ -40,7 +40,7 @@ export default [
&.added {
@media (prefers-reduced-motion: no-preference) {
- animation: fade-in var(--animation-duration) ease-in-out;
+ animation: fade-in var(--private-animation-duration) ease-in-out;
}
}
@@ -50,7 +50,7 @@ export default [
&.removed {
@media (prefers-reduced-motion: no-preference) {
- animation-duration: var(--animation-duration);
+ animation-duration: var(--private-animation-duration);
animation-fill-mode: forwards;
animation-name: fade-out;
animation-timing-function: ease-in-out;
@@ -167,13 +167,13 @@ export default [
}
&.medium {
- --size: 0.75rem;
+ --private-size: 0.75rem;
margin-inline-start: 0.375rem;
}
&.small {
- --size: 0.625rem;
+ --private-size: 0.625rem;
margin-inline-start: var(--glide-core-spacing-xxs);
}
diff --git a/src/tag.ts b/src/tag.ts
index 948a7c4c0..d42995e4f 100644
--- a/src/tag.ts
+++ b/src/tag.ts
@@ -83,7 +83,7 @@ export default class GlideCoreTag extends LitElement {
})}
data-test="component"
data-animation-duration=${this.#animationDuration}
- style="--animation-duration: ${this.#animationDuration}ms"
+ style="--private-animation-duration: ${this.#animationDuration}ms"
${ref(this.#componentElementRef)}
>
+ >
+
+
${this.label}
${when(this.privateEditable, () => {
diff --git a/src/textarea.styles.ts b/src/textarea.styles.ts
index 11f66f7ed..e272c46ec 100644
--- a/src/textarea.styles.ts
+++ b/src/textarea.styles.ts
@@ -1,16 +1,16 @@
import { css, unsafeCSS } from 'lit';
import visuallyHidden from './styles/visually-hidden.js';
-/**
- * `field-sizing` is only supported in Chrome and Edge
- * at the moment (https://caniuse.com/mdn-css_properties_field-sizing),
- * making this a progressive enhancement. This functionality is additive,
- * rather than required for use with our components.
- *
- * `field-sizing` is also not recognized by lit-plugin, so we are seeing
- * https://github.com/runem/lit-analyzer/issues/157 when
- * attempting to use it directly in our CSS below. This use of unsafeCSS
- * is a workaround for that bug for the time being.
+/*
+ `field-sizing` is only supported in Chrome and Edge
+ at the moment (https://caniuse.com/mdn-css_properties_field-sizing),
+ making this a progressive enhancement. This functionality is additive,
+ rather than required for use with our components.
+
+ `field-sizing` is also not recognized by lit-plugin, so we are seeing
+ https://github.com/runem/lit-analyzer/issues/157 when
+ attempting to use it directly in our CSS below. This use of unsafeCSS
+ is a workaround for that bug for the time being.
*/
const fieldSizingContent = unsafeCSS(`
field-sizing: content;
diff --git a/src/textarea.ts b/src/textarea.ts
index 7895b34f4..25e442086 100644
--- a/src/textarea.ts
+++ b/src/textarea.ts
@@ -112,7 +112,7 @@ export default class GlideCoreTextarea
@property({ reflect: true })
readonly version = packageJson.version;
- checkValidity() {
+ checkValidity(): boolean {
this.isCheckingValidity = true;
const isValid = this.#internals.checkValidity();
this.isCheckingValidity = false;
@@ -126,11 +126,11 @@ export default class GlideCoreTextarea
this.form?.removeEventListener('formdata', this.#onFormdata);
}
- get form() {
+ get form(): HTMLFormElement | null {
return this.#internals.form;
}
- get validity() {
+ get validity(): ValidityState {
if (this.required && !this.value && !this.disabled) {
// A validation message is required but unused because we disable native validation feedback.
// And an empty string isn't allowed. Thus a single space.
@@ -160,11 +160,11 @@ export default class GlideCoreTextarea
return this.#internals.validity;
}
- formAssociatedCallback() {
+ formAssociatedCallback(): void {
this.form?.addEventListener('formdata', this.#onFormdata);
}
- formResetCallback() {
+ formResetCallback(): void {
this.value = this.getAttribute('value') ?? '';
}
@@ -222,7 +222,12 @@ export default class GlideCoreTextarea
),
})}
name="description"
- >
+ >
+
+
${when(
this.#isShowValidationFeedback && this.validityMessage,
@@ -260,7 +265,7 @@ export default class GlideCoreTextarea
>`;
}
- reportValidity() {
+ reportValidity(): boolean {
this.isReportValidityOrSubmit = true;
const isValid = this.#internals.reportValidity();
@@ -271,11 +276,11 @@ export default class GlideCoreTextarea
return isValid;
}
- resetValidityFeedback() {
+ resetValidityFeedback(): void {
this.isReportValidityOrSubmit = false;
}
- setCustomValidity(message: string) {
+ setCustomValidity(message: string): void {
this.validityMessage = message;
if (message === '') {
@@ -296,7 +301,7 @@ export default class GlideCoreTextarea
}
}
- setValidity(flags?: ValidityStateFlags, message?: string) {
+ setValidity(flags?: ValidityStateFlags, message?: string): void {
this.validityMessage = message;
this.#internals.setValidity(flags, ' ', this.#textareaElementRef.value);
@@ -399,16 +404,14 @@ export default class GlideCoreTextarea
this.isBlurring = false;
}
- #onTextareaChange(event: Event) {
+ #onTextareaChange() {
if (this.#textareaElementRef.value) {
this.value = this.#textareaElementRef.value.value;
}
// Unlike "input" events, "change" events aren't composed. So we have to
// manually dispatch them.
- this.dispatchEvent(
- new Event(event.type, { bubbles: true, composed: true }),
- );
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
}
#onTextareaInput() {
diff --git a/src/toasts.toast.styles.ts b/src/toasts.toast.styles.ts
index 91d3e3355..8bd00cfe0 100644
--- a/src/toasts.toast.styles.ts
+++ b/src/toasts.toast.styles.ts
@@ -65,7 +65,7 @@ export default [
}
.close-button {
- --icon-color: var(--glide-core-icon-default2);
+ --private-icon-color: var(--glide-core-icon-default2);
grid-column: 3;
}
diff --git a/src/toasts.toast.ts b/src/toasts.toast.ts
index cf8bcde34..e2e76da18 100644
--- a/src/toasts.toast.ts
+++ b/src/toasts.toast.ts
@@ -8,7 +8,6 @@ import { styleMap } from 'lit/directives/style-map.js';
import { LocalizeController } from './library/localize.js';
import styles from './toasts.toast.styles.js';
import xIcon from './icons/x.js';
-import type { Toast } from './toasts.js';
import shadowRootMode from './library/shadow-root-mode.js';
import final from './library/final.js';
@@ -41,9 +40,9 @@ export default class GlideCoreToast extends LitElement {
duration? = 5000;
@property()
- variant?: Toast['variant'];
+ variant?: 'error' | 'informational' | 'success';
- close() {
+ close(): void {
this.#componentElementRef.value?.addEventListener(
'transitionend',
() => {
@@ -64,7 +63,7 @@ export default class GlideCoreToast extends LitElement {
});
}
- open() {
+ open(): void {
const duration = Math.max(this.duration ?? 0, 5000);
if (duration < Number.POSITIVE_INFINITY) {
diff --git a/src/toasts.ts b/src/toasts.ts
index 4941f9806..3cd077ef0 100644
--- a/src/toasts.ts
+++ b/src/toasts.ts
@@ -1,4 +1,3 @@
-import './toasts.toast.js';
import { html, LitElement } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js';
import { customElement, property } from 'lit/decorators.js';
@@ -7,6 +6,7 @@ import { LocalizeController } from './library/localize.js';
import styles from './toasts.styles.js';
import shadowRootMode from './library/shadow-root-mode.js';
import final from './library/final.js';
+import GlideCoreToast from './toasts.toast.js';
declare global {
interface HTMLElementTagNameMap {
@@ -14,13 +14,6 @@ declare global {
}
}
-export interface Toast {
- label: string;
- description: string;
- variant: 'error' | 'informational' | 'success';
- duration?: number;
-}
-
@customElement('glide-core-toasts')
@final
export default class GlideCoreToasts extends LitElement {
@@ -34,12 +27,12 @@ export default class GlideCoreToasts extends LitElement {
@property({ reflect: true })
readonly version = packageJson.version;
- /**
- * @param {number} [toast.duration=5000]
- * Optional: Number of milliseconds before the Toast auto-hides.
- * Minimum: `5000`. Default: `5000`. For a Toast that never auto-hides, set to `Infinity`
- * */
- add(toast: Toast) {
+ add(toast: {
+ label: string;
+ description: string;
+ variant: 'error' | 'informational' | 'success';
+ duration?: number; // Defaults to 5000. Set to `Infinity` to make the toast persist until dismissed.
+ }): GlideCoreToast {
const { variant, label, description, duration } = toast;
const toastElement = Object.assign(
diff --git a/src/toggle.styles.ts b/src/toggle.styles.ts
index 998f36f95..f1ff14109 100644
--- a/src/toggle.styles.ts
+++ b/src/toggle.styles.ts
@@ -13,7 +13,7 @@ export default [
}
.toggle-and-input {
- --inline-size: 1.5rem;
+ --private-inline-size: 1.5rem;
align-items: center;
background-color: var(--glide-core-surface-selected-disabled);
@@ -22,7 +22,7 @@ export default [
border-radius: var(--glide-core-spacing-sm);
display: flex;
flex-shrink: 0; /* Don't shrink when the summary wraps. */
- inline-size: var(--inline-size);
+ inline-size: var(--private-inline-size);
justify-content: center;
position: relative;
@@ -63,7 +63,7 @@ export default [
inline-size: 0.875rem;
inset-inline-end: 0;
position: absolute;
- transform: translateX(calc(var(--inline-size) * -1 + 100%));
+ transform: translateX(calc(var(--private-inline-size) * -1 + 100%));
transition: 150ms transform;
}
}
diff --git a/src/toggle.ts b/src/toggle.ts
index 029f1ddfd..a5bfb672b 100644
--- a/src/toggle.ts
+++ b/src/toggle.ts
@@ -50,9 +50,6 @@ export default class GlideCoreToggle extends LitElement {
@property({ reflect: true })
orientation: 'horizontal' | 'vertical' = 'horizontal';
- @property({ reflect: true })
- name?: string;
-
// Private because it's only meant to be used by Form Controls Layout.
@property()
privateSplit?: 'left' | 'middle';
@@ -129,7 +126,12 @@ export default class GlideCoreToggle extends LitElement {
id="description"
name="description"
slot="description"
- >
+ >
+
+
`;
}
@@ -163,7 +165,7 @@ export default class GlideCoreToggle extends LitElement {
// Unlike "input" events, "change" events aren't composed. So we have to
// manually dispatch them.
this.dispatchEvent(
- new Event(event.type, { bubbles: true, composed: true }),
+ new Event('change', { bubbles: true, composed: true }),
);
}
}
diff --git a/src/tooltip.container.ts b/src/tooltip.container.ts
index 501393859..437c25b3a 100644
--- a/src/tooltip.container.ts
+++ b/src/tooltip.container.ts
@@ -2,7 +2,6 @@ import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { nanoid } from 'nanoid';
import { classMap } from 'lit/directives/class-map.js';
-import { type Placement } from '@floating-ui/dom';
import { map } from 'lit/directives/map.js';
import styles from './tooltip.container.styles.js';
import shadowRootMode from './library/shadow-root-mode.js';
@@ -41,8 +40,11 @@ export default class GlideCoreTooltipContainer extends LitElement {
static override styles = styles;
+ /**
+ * @default false
+ */
@property({ type: Boolean })
- get disabled() {
+ get disabled(): boolean {
return this.#isDisabled;
}
@@ -55,9 +57,9 @@ export default class GlideCoreTooltipContainer extends LitElement {
label?: string;
@property()
- placement?: Placement;
+ placement?: 'bottom' | 'left' | 'right' | 'top';
- @property({ type: Boolean })
+ @property({ attribute: 'screenreader-hidden', type: Boolean })
screenreaderHidden = false;
@property({ type: Array })
diff --git a/src/tooltip.styles.ts b/src/tooltip.styles.ts
index a76a75844..fa16f1e27 100644
--- a/src/tooltip.styles.ts
+++ b/src/tooltip.styles.ts
@@ -9,7 +9,7 @@ export default [
`,
css`
:host {
- // https://github.com/CrowdStrike/glide-core/pull/307/files#r1718545771
+ /* https://github.com/CrowdStrike/glide-core/pull/307/files#r1718545771 */
display: inline-block;
}
@@ -80,8 +80,8 @@ export default [
}
.arrow {
- --arrow-height: 0.375rem;
- --arrow-width: 0.625rem;
+ --private-arrow-height: 0.375rem;
+ --private-arrow-width: 0.625rem;
color: var(--glide-core-surface-base-dark);
display: flex;
@@ -89,14 +89,14 @@ export default [
&.top,
&.bottom {
- block-size: var(--arrow-height);
- inline-size: var(--arrow-width);
+ block-size: var(--private-arrow-height);
+ inline-size: var(--private-arrow-width);
}
&.right,
&.left {
- block-size: var(--arrow-width);
- inline-size: var(--arrow-height);
+ block-size: var(--private-arrow-width);
+ inline-size: var(--private-arrow-height);
order: 2;
}
}
diff --git a/src/tooltip.ts b/src/tooltip.ts
index 521db42ce..2e02a5596 100644
--- a/src/tooltip.ts
+++ b/src/tooltip.ts
@@ -44,8 +44,11 @@ export default class GlideCoreTooltip extends LitElement {
static override styles = styles;
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get disabled() {
+ get disabled(): boolean {
return this.#isDisabled;
}
@@ -75,6 +78,9 @@ export default class GlideCoreTooltip extends LitElement {
}
}
+ /**
+ * @default undefined
+ */
@property({ reflect: true })
@required
get label(): string | undefined {
@@ -93,8 +99,11 @@ export default class GlideCoreTooltip extends LitElement {
}
}
+ /**
+ * @default 4
+ */
@property({ reflect: true, type: Number })
- get offset() {
+ get offset(): number {
return (
this.#offset ??
Number.parseFloat(
@@ -112,11 +121,17 @@ export default class GlideCoreTooltip extends LitElement {
this.#offset = offset;
}
+ /**
+ * @default false
+ */
@property({ reflect: true, type: Boolean })
- get open() {
+ get open(): boolean {
return this.#isOpen;
}
+ /**
+ * @default false
+ */
set open(isOpen: boolean) {
const hasChanged = isOpen !== this.#isOpen;
this.#isOpen = isOpen;
@@ -136,15 +151,18 @@ export default class GlideCoreTooltip extends LitElement {
}
}
- /*
- The placement of the tooltip relative to its target. Automatic placement will
- take over if the tooltip is cut off by the viewport. "bottom" by default.
- */
+ /**
+ * The placement of the tooltip relative to its target. Automatic placement will
+ * take over if the tooltip is cut off by the viewport.
+ */
@property({ reflect: true })
placement?: 'bottom' | 'left' | 'right' | 'top';
+ /**
+ * @default false
+ */
@property({ attribute: 'screenreader-hidden', reflect: true, type: Boolean })
- get screenreaderHidden() {
+ get screenreaderHidden(): boolean {
return this.#isScreenreaderHidden;
}
@@ -168,8 +186,11 @@ export default class GlideCoreTooltip extends LitElement {
}
}
+ /**
+ * @default []
+ */
@property({ reflect: true, type: Array })
- get shortcut() {
+ get shortcut(): string[] {
return this.#shortcut;
}
@@ -251,7 +272,15 @@ export default class GlideCoreTooltip extends LitElement {
${assertSlot()}
${ref(this.#targetSlotElementRef)}
name="target"
- >
+ >
+
+
-
+
+
+
@@ -456,7 +489,13 @@ export default class GlideCoreTooltip extends LitElement {
'glide-core-private-tooltip-container',
);
- if (container) {
+ const isSupportedPlacement =
+ placement === 'bottom' ||
+ placement === 'left' ||
+ placement === 'right' ||
+ placement === 'top';
+
+ if (container && isSupportedPlacement) {
container.placement = placement;
}
}
diff --git a/src/tree.item.icon-button.styles.ts b/src/tree.item.icon-button.styles.ts
index 3a0cfae4e..77bff957b 100644
--- a/src/tree.item.icon-button.styles.ts
+++ b/src/tree.item.icon-button.styles.ts
@@ -5,9 +5,9 @@ export default [
.component {
display: flex;
- --icon-color: var(--icon-button-color);
- --hovered-icon-color: var(--hovered-icon-button-color);
- --size: 1rem;
+ --private-icon-color: var(--private-icon-button-color);
+ --private-hovered-icon-color: var(--private-hovered-icon-button-color);
+ --private-size: 1rem;
}
`,
];
diff --git a/src/tree.item.icon-button.ts b/src/tree.item.icon-button.ts
index 63e29e57b..9cf5f9e05 100644
--- a/src/tree.item.icon-button.ts
+++ b/src/tree.item.icon-button.ts
@@ -43,7 +43,12 @@ export default class GlideCoreTreeItemIconButton extends LitElement {
tabindex="-1"
label=${ifDefined(this.label)}
>
-
+
+
+
`;
}
diff --git a/src/tree.item.menu.styles.ts b/src/tree.item.menu.styles.ts
index 8d0c72e06..3564481e2 100644
--- a/src/tree.item.menu.styles.ts
+++ b/src/tree.item.menu.styles.ts
@@ -13,9 +13,9 @@ export default [
glide-core-icon-button {
display: flex;
- --icon-color: var(--icon-button-color);
- --hovered-icon-color: var(--hovered-icon-button-color);
- --size: 1rem;
+ --private-icon-color: var(--private-icon-button-color);
+ --private-hovered-icon-color: var(--private-hovered-icon-button-color);
+ --private-size: 1rem;
}
`,
];
diff --git a/src/tree.item.menu.ts b/src/tree.item.menu.ts
index 95b2de698..13873de3e 100644
--- a/src/tree.item.menu.ts
+++ b/src/tree.item.menu.ts
@@ -5,6 +5,7 @@ import { createRef, ref } from 'lit/directives/ref.js';
import { customElement, property, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
+import { ifDefined } from 'lit/directives/if-defined.js';
import packageJson from '../package.json' with { type: 'json' };
import styles from './tree.item.menu.styles.js';
import GlideCoreIconButton from './icon-button.js';
@@ -39,7 +40,7 @@ export default class GlideCoreTreeItemMenu extends LitElement {
placement: 'bottom-start' | 'top-start' = 'bottom-start';
@property()
- label = '';
+ label?: string;
@property({ reflect: true })
readonly version = packageJson.version;
@@ -56,21 +57,25 @@ export default class GlideCoreTreeItemMenu extends LitElement {
${ref(this.#menuElementRef)}
>
-
+
+
+
+ >
+
+
${when(!this.hasCustomIcon, () => icons.dots)}
diff --git a/src/tree.item.styles.ts b/src/tree.item.styles.ts
index bd4099b14..a66bcbc57 100644
--- a/src/tree.item.styles.ts
+++ b/src/tree.item.styles.ts
@@ -20,15 +20,15 @@ export default [
grid-template-columns: repeat(auto-fill, 2.5rem);
line-height: 1.25rem;
- --color: var(--glide-core-text-body-1);
+ --private-color: var(--glide-core-text-body-1);
&.selected {
- --color: var(--glide-core-text-selected);
+ --private-color: var(--glide-core-text-selected);
::slotted([slot='prefix']),
::slotted([slot='menu']),
::slotted([slot='suffix']) {
- --hovered-icon-button-color: var(--glide-core-icon-hover);
+ --private-hovered-icon-button-color: var(--glide-core-icon-hover);
}
}
}
@@ -69,7 +69,7 @@ export default [
.label-container {
align-items: center;
border-radius: 0.5rem;
- color: var(--color);
+ color: var(--private-color);
display: flex;
font-size: var(--glide-core-body-sm-font-size);
padding-block: var(--glide-core-spacing-xxs);
@@ -111,7 +111,7 @@ export default [
::slotted([slot='prefix']),
::slotted([slot='menu']),
::slotted([slot='suffix']) {
- --icon-button-color: var(--color);
+ --private-icon-button-color: var(--private-color);
}
.label-container:hover,
diff --git a/src/tree.item.ts b/src/tree.item.ts
index a3d5d5ce6..560892b7a 100644
--- a/src/tree.item.ts
+++ b/src/tree.item.ts
@@ -50,7 +50,24 @@ export default class GlideCoreTreeItem extends LitElement {
@property({ reflect: true, type: Number }) level = 1;
- @property({ reflect: true, type: Boolean }) selected = false;
+ /**
+ * @default false
+ */
+ @property({ reflect: true, type: Boolean })
+ get selected(): boolean {
+ return this.#isSelected;
+ }
+
+ set selected(isSelected: boolean) {
+ const hasChanged = isSelected !== this.#isSelected;
+ this.#isSelected = isSelected;
+
+ if (isSelected && hasChanged) {
+ this.dispatchEvent(
+ new Event('selected', { bubbles: true, composed: true }),
+ );
+ }
+ }
@property({ reflect: true, type: Boolean, attribute: 'remove-indentation' })
removeIndentation = false;
@@ -66,12 +83,16 @@ export default class GlideCoreTreeItem extends LitElement {
this.#setTabIndexes(0);
}
- get hasChildTreeItems() {
+ get privateHasChildTreeItems() {
return this.childTreeItems.length > 0;
}
- get hasExpandIcon() {
- return this.hasChildTreeItems && !this.nonCollapsible;
+ private get hasExpandIcon() {
+ return this.privateHasChildTreeItems && !this.nonCollapsible;
+ }
+
+ privateToggleExpand() {
+ this.expanded = !this.expanded;
}
override render() {
@@ -128,7 +149,12 @@ export default class GlideCoreTreeItem extends LitElement {
class="prefix-slot"
${ref(this.#prefixSlotElementRef)}
@slotchange=${this.#onPrefixSlotChange}
- >
+ >
+
+
-
-
+ >
+
+
+
+
+
+
@@ -161,15 +197,15 @@ export default class GlideCoreTreeItem extends LitElement {
+ >
+
+
`;
}
/**
- * Traverses down the tree, selecting the passed-in item,
- * and deselecting all other items.
- * Returns the selected item
+ * Traverses down the tree, selects the item, then deselects all other items.
*/
selectItem(item: GlideCoreTreeItem): GlideCoreTreeItem | undefined {
let selectedItem;
@@ -196,10 +232,6 @@ export default class GlideCoreTreeItem extends LitElement {
return selectedItem;
}
- toggleExpand() {
- this.expanded = !this.expanded;
- }
-
@state()
private childTreeItems: GlideCoreTreeItem[] = [];
@@ -208,6 +240,8 @@ export default class GlideCoreTreeItem extends LitElement {
#defaultSlotElementRef = createRef();
+ #isSelected = false;
+
#labelContainerElementRef = createRef();
#localize = new LocalizeController(this);
@@ -226,7 +260,7 @@ export default class GlideCoreTreeItem extends LitElement {
}
get #ariaExpanded() {
- if (this.hasChildTreeItems) {
+ if (this.privateHasChildTreeItems) {
return this.expanded ? 'true' : 'false';
} else {
return;
@@ -234,7 +268,7 @@ export default class GlideCoreTreeItem extends LitElement {
}
get #ariaSelected() {
- if (this.hasChildTreeItems) {
+ if (this.privateHasChildTreeItems) {
return;
} else {
return this.selected ? 'true' : 'false';
diff --git a/src/tree.test.basics.ts b/src/tree.test.basics.ts
index 321c82377..63688d2af 100644
--- a/src/tree.test.basics.ts
+++ b/src/tree.test.basics.ts
@@ -54,7 +54,7 @@ it('sets `aria-expanded`', async () => {
items[1].shadowRoot?.querySelector('[data-test="component"]')?.ariaExpanded,
).to.equal('false');
- items[1].toggleExpand();
+ items[1].privateToggleExpand();
await items[1].updateComplete;
expect(
diff --git a/src/tree.ts b/src/tree.ts
index e4b99a4ba..b34aea251 100644
--- a/src/tree.ts
+++ b/src/tree.ts
@@ -48,11 +48,13 @@ export default class GlideCoreTree extends LitElement {
+ >
+
+
`;
}
- selectItem(item: GlideCoreTreeItem) {
+ selectItem(item: GlideCoreTreeItem): void {
if (this.#treeItemElements) {
for (const treeItem of this.#treeItemElements) {
if (item === treeItem) {
@@ -70,10 +72,6 @@ export default class GlideCoreTree extends LitElement {
this.selectedItem = nestedSelectedItem;
}
}
-
- item.dispatchEvent(
- new Event('selected', { bubbles: true, composed: true }),
- );
}
}
@@ -135,8 +133,8 @@ export default class GlideCoreTree extends LitElement {
const clickedItem = target.closest('glide-core-tree-item');
if (clickedItem) {
- if (clickedItem.hasChildTreeItems && !clickedItem.nonCollapsible) {
- clickedItem.toggleExpand();
+ if (clickedItem.privateHasChildTreeItems && !clickedItem.nonCollapsible) {
+ clickedItem.privateToggleExpand();
} else {
this.selectItem(clickedItem);
}
@@ -189,17 +187,17 @@ export default class GlideCoreTree extends LitElement {
item.matches(':focus'),
);
- if (event.key === 'ArrowRight' && focusedItem?.hasChildTreeItems) {
+ if (event.key === 'ArrowRight' && focusedItem?.privateHasChildTreeItems) {
if (focusedItem.expanded) {
this.#focusItem(focusedItem.querySelector('glide-core-tree-item'));
} else {
- focusedItem.toggleExpand();
+ focusedItem.privateToggleExpand();
}
}
if (event.key === 'ArrowLeft') {
if (focusedItem?.expanded && !focusedItem.nonCollapsible) {
- focusedItem.toggleExpand();
+ focusedItem.privateToggleExpand();
} else {
const parentTreeItem = focusedItem?.parentElement?.closest(
'glide-core-tree-item',
@@ -230,8 +228,8 @@ export default class GlideCoreTree extends LitElement {
}
if (event.key === 'Enter' && focusedItem) {
- if (focusedItem.hasChildTreeItems && !focusedItem.nonCollapsible) {
- focusedItem.toggleExpand();
+ if (focusedItem.privateHasChildTreeItems && !focusedItem.nonCollapsible) {
+ focusedItem.privateToggleExpand();
} else {
this.selectItem(focusedItem);
}
diff --git a/terser.js b/terser.js
index cc1b464bb..afbf78b7a 100644
--- a/terser.js
+++ b/terser.js
@@ -3,7 +3,17 @@ import { globby } from 'globby';
import { minify } from 'terser';
import { minifyHTMLLiterals } from 'minify-literals';
-const paths = await globby(['dist/**/*.js', '!**/*stories*', '!**/*test*']);
+const paths = await globby([
+ 'dist/**/*.js',
+ '!**/.storybook/**',
+ '!**/*stories*',
+ '!**/*test*',
+ '!**/cem-analyzer-plugins/**',
+ '!**/coverage/**',
+ '!**/eslint/**',
+ '!**/stylelint/**',
+ '!**/ts-morph*/**',
+]);
await paths.map(async (path) => {
const unminified = await readFile(path, 'utf8');
diff --git a/tsconfig.json b/tsconfig.json
index 055d0abcd..7fba051dc 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,8 +8,8 @@
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
- // So our import of `package.json` in various files doesn't cause our
- // `./src` files in `./dist` to be nested under `./dist/src`.
+ // So that our import of `package.json` in various files doesn't cause
+ // our compiled `./src` files to be nested under `./dist/src`.
"rootDir": "./src",
"useDefineForClassFields": false, // https://lit.dev/docs/components/decorators#decorators-typescript
"verbatimModuleSyntax": true,
diff --git a/vitest.config.js b/vitest.config.js
index 901a89eb0..6e9cb6595 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -5,6 +5,6 @@ export default defineConfig({
globals: true,
environment: 'node',
setupFiles: ['./vitest-setup.js'],
- include: ['**/eslint/**/*.test.ts'],
+ include: ['**/eslint/**/*.test.ts', '**/stylelint/**/*.test.ts'],
},
});
diff --git a/web-test-runner.config.js b/web-test-runner.config.js
index 8bd30dfe2..339c59ca7 100644
--- a/web-test-runner.config.js
+++ b/web-test-runner.config.js
@@ -34,7 +34,14 @@ export default {
lines: 100,
},
},
- files: ['src/**/*.test.ts', 'src/**/*.test.*.ts', '!**/eslint/**'],
+ files: [
+ 'src/**/*.test.ts',
+ 'src/**/*.test.*.ts',
+ '!**/eslint/**',
+ '!**/icons/**',
+ '!**/stylelint/**',
+ '!**/translations/**',
+ ],
nodeResolve: {
// Ow is an example of a module that supports both the browser and Node.js
// and uses the `browser` field in `package.json` to switch between artifacts.