@api-client/ui 0.5.47 → 0.5.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/elements/data-table/DataTable.d.ts +1 -1
- package/build/src/elements/data-table/DataTable.js +3 -3
- package/build/src/elements/data-table/DataTable.js.map +1 -1
- package/build/src/elements/file-system/internals/Breadcrumbs.d.ts +1 -1
- package/build/src/elements/file-system/internals/Breadcrumbs.d.ts.map +1 -1
- package/build/src/elements/file-system/internals/Breadcrumbs.js +3 -3
- package/build/src/elements/file-system/internals/Breadcrumbs.js.map +1 -1
- package/build/src/elements/navigation/internals/NavigationItem.d.ts +2 -0
- package/build/src/elements/navigation/internals/NavigationItem.d.ts.map +1 -1
- package/build/src/elements/navigation/internals/NavigationItem.js +4 -2
- package/build/src/elements/navigation/internals/NavigationItem.js.map +1 -1
- package/build/src/elements/user/internals/UserAvatar.d.ts +26 -4
- package/build/src/elements/user/internals/UserAvatar.d.ts.map +1 -1
- package/build/src/elements/user/internals/UserAvatar.js +125 -12
- package/build/src/elements/user/internals/UserAvatar.js.map +1 -1
- package/build/src/elements/user/internals/UserAvatar.styles.d.ts.map +1 -1
- package/build/src/elements/user/internals/UserAvatar.styles.js +6 -11
- package/build/src/elements/user/internals/UserAvatar.styles.js.map +1 -1
- package/build/src/md/button/internals/base.d.ts +1 -1
- package/build/src/md/button/internals/base.d.ts.map +1 -1
- package/build/src/md/button/internals/base.js +2 -2
- package/build/src/md/button/internals/base.js.map +1 -1
- package/build/src/md/checkbox/internals/CheckboxElement.d.ts +1 -1
- package/build/src/md/checkbox/internals/CheckboxElement.d.ts.map +1 -1
- package/build/src/md/checkbox/internals/CheckboxElement.js +2 -2
- package/build/src/md/checkbox/internals/CheckboxElement.js.map +1 -1
- package/build/src/md/focus-ring/internals/focus-ring.d.ts +87 -0
- package/build/src/md/focus-ring/internals/focus-ring.d.ts.map +1 -0
- package/build/src/md/focus-ring/internals/focus-ring.js +206 -0
- package/build/src/md/focus-ring/internals/focus-ring.js.map +1 -0
- package/build/src/md/focus-ring/internals/focus-ring.styles.d.ts +3 -0
- package/build/src/md/focus-ring/internals/focus-ring.styles.d.ts.map +1 -0
- package/build/src/md/focus-ring/internals/focus-ring.styles.js +109 -0
- package/build/src/md/focus-ring/internals/focus-ring.styles.js.map +1 -0
- package/build/src/md/focus-ring/ui-focus-ring.d.ts +42 -0
- package/build/src/md/focus-ring/ui-focus-ring.d.ts.map +1 -0
- package/build/src/md/focus-ring/ui-focus-ring.js +58 -0
- package/build/src/md/focus-ring/ui-focus-ring.js.map +1 -0
- package/build/src/md/list/internals/ListItem.d.ts +3 -3
- package/build/src/md/list/internals/ListItem.d.ts.map +1 -1
- package/build/src/md/list/internals/ListItem.js +7 -6
- package/build/src/md/list/internals/ListItem.js.map +1 -1
- package/build/src/md/menu/internal/MenuItem.d.ts +0 -2
- package/build/src/md/menu/internal/MenuItem.d.ts.map +1 -1
- package/build/src/md/menu/internal/MenuItem.js +0 -2
- package/build/src/md/menu/internal/MenuItem.js.map +1 -1
- package/build/src/md/radio/internals/Radio.styles.js +1 -1
- package/build/src/md/radio/internals/Radio.styles.js.map +1 -1
- package/build/src/md/radio/internals/RadioElement.d.ts +1 -0
- package/build/src/md/radio/internals/RadioElement.d.ts.map +1 -1
- package/build/src/md/radio/internals/RadioElement.js +2 -0
- package/build/src/md/radio/internals/RadioElement.js.map +1 -1
- package/build/src/md/select/internals/Select.d.ts +1 -1
- package/build/src/md/select/internals/Select.d.ts.map +1 -1
- package/build/src/md/select/internals/Select.js +2 -2
- package/build/src/md/select/internals/Select.js.map +1 -1
- package/build/src/md/tabs/internals/Tab.d.ts +1 -0
- package/build/src/md/tabs/internals/Tab.d.ts.map +1 -1
- package/build/src/md/tabs/internals/Tab.js +3 -2
- package/build/src/md/tabs/internals/Tab.js.map +1 -1
- package/build/src/md/tabs/internals/Tabs.js +4 -4
- package/build/src/md/tabs/internals/Tabs.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -2
- package/src/elements/data-table/DataTable.ts +3 -3
- package/src/elements/file-system/internals/Breadcrumbs.ts +3 -3
- package/src/elements/navigation/internals/NavigationItem.ts +4 -2
- package/src/elements/user/internals/UserAvatar.styles.ts +6 -11
- package/src/elements/user/internals/UserAvatar.ts +115 -8
- package/src/md/button/internals/base.ts +2 -2
- package/src/md/checkbox/internals/CheckboxElement.ts +2 -2
- package/src/md/focus-ring/internals/focus-ring.styles.ts +109 -0
- package/src/md/focus-ring/internals/focus-ring.ts +184 -0
- package/src/md/focus-ring/ui-focus-ring.ts +46 -0
- package/src/md/list/internals/ListItem.ts +5 -5
- package/src/md/menu/internal/MenuItem.ts +0 -2
- package/src/md/radio/internals/Radio.styles.ts +1 -1
- package/src/md/radio/internals/RadioElement.ts +2 -0
- package/src/md/select/internals/Select.ts +2 -2
- package/src/md/tabs/internals/Tab.ts +4 -2
- package/src/md/tabs/internals/Tabs.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@api-client/ui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.48",
|
|
4
4
|
"description": "Internal UI component library for the API Client ecosystem.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"main": "build/src/index.js",
|
|
@@ -188,7 +188,6 @@
|
|
|
188
188
|
"@codemirror/state": "^6.5.2",
|
|
189
189
|
"@codemirror/view": "^6.38.0",
|
|
190
190
|
"@github/relative-time-element": "^4.4.6",
|
|
191
|
-
"@material/web": "^2.3.0",
|
|
192
191
|
"@pawel-up/jexl": "^4.3.0",
|
|
193
192
|
"@types/har-format": "^1.2.8",
|
|
194
193
|
"dompurify": "^3.2.5",
|
|
@@ -7,7 +7,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'
|
|
|
7
7
|
import { dataAttr } from '../../directives/data-attr.js'
|
|
8
8
|
import { LiveData } from '../../reactive/LiveData.js'
|
|
9
9
|
|
|
10
|
-
import '
|
|
10
|
+
import '../../md/focus-ring/ui-focus-ring.js'
|
|
11
11
|
import '../../md/icons/ui-icon.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -929,7 +929,7 @@ export abstract class DataTable<T extends object> extends LitElement {
|
|
|
929
929
|
index = isFocused ? 0 : -1
|
|
930
930
|
}
|
|
931
931
|
const ring = !delegateFocus
|
|
932
|
-
? html`<
|
|
932
|
+
? html`<ui-focus-ring .inward="${this.overflow ? true : false}"></ui-focus-ring>`
|
|
933
933
|
: nothing
|
|
934
934
|
if (isPrimary) {
|
|
935
935
|
// Lit does not allow dynamic element names so I decided to make a copy.
|
|
@@ -1020,7 +1020,7 @@ export abstract class DataTable<T extends object> extends LitElement {
|
|
|
1020
1020
|
aria-label="${ifDefined(ariaLabel)}"
|
|
1021
1021
|
part="header-cell"
|
|
1022
1022
|
>
|
|
1023
|
-
<
|
|
1023
|
+
<ui-focus-ring .inward="${this.overflow ? true : false}"></ui-focus-ring>
|
|
1024
1024
|
<div class="cell-content">
|
|
1025
1025
|
${content}
|
|
1026
1026
|
${isSortable && sortIcon ? html`<ui-icon class="sort-icon" role="presentation">${sortIcon}</ui-icon>` : nothing}
|
|
@@ -4,7 +4,7 @@ import { LiveData } from '../../../reactive/LiveData.js'
|
|
|
4
4
|
import { type FileBreadcrumb } from '@api-client/core/models/store/File.js'
|
|
5
5
|
import { FolderKind } from '@api-client/core/models/kinds.js'
|
|
6
6
|
|
|
7
|
-
import '
|
|
7
|
+
import '../../../md/focus-ring/ui-focus-ring.js'
|
|
8
8
|
import '../../../md/icons/ui-icon.js'
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -123,7 +123,7 @@ export default class Breadcrumbs extends LitElement {
|
|
|
123
123
|
class="link-button"
|
|
124
124
|
@click="${this.handleLinkClick}"
|
|
125
125
|
>
|
|
126
|
-
<
|
|
126
|
+
<ui-focus-ring part="focus-ring"></ui-focus-ring>
|
|
127
127
|
${name}
|
|
128
128
|
</div>
|
|
129
129
|
`
|
|
@@ -133,7 +133,7 @@ export default class Breadcrumbs extends LitElement {
|
|
|
133
133
|
const { key, kind, name } = item
|
|
134
134
|
return html`
|
|
135
135
|
<div tabindex="0" role="button" data-key="${key}" data-kind="${kind}" class="action-button" aria-current="page">
|
|
136
|
-
<
|
|
136
|
+
<ui-focus-ring part="focus-ring"></ui-focus-ring>
|
|
137
137
|
${name}
|
|
138
138
|
</div>
|
|
139
139
|
`
|
|
@@ -4,6 +4,8 @@ import { classMap } from 'lit/directives/class-map.js'
|
|
|
4
4
|
import { when } from 'lit/directives/when.js'
|
|
5
5
|
import { UiElement } from '../../../md/UiElement.js'
|
|
6
6
|
import { isDisabled, setDisabled } from '../../../lib/disabled.js'
|
|
7
|
+
import '../../../md/focus-ring/ui-focus-ring.js'
|
|
8
|
+
import '../../../md/ripple/ui-ripple.js'
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* NavigationItem
|
|
@@ -120,8 +122,8 @@ export default class NavigationItem extends UiElement {
|
|
|
120
122
|
})
|
|
121
123
|
return html`
|
|
122
124
|
<button class="${containerClasses}" id="button" ?disabled="${this.disabled}" aria-disabled="${this.disabled}">
|
|
123
|
-
<
|
|
124
|
-
<
|
|
125
|
+
<ui-focus-ring part="focus-ring" for="button"></ui-focus-ring>
|
|
126
|
+
<ui-ripple></ui-ripple>
|
|
125
127
|
${this.renderIcon()}${when(
|
|
126
128
|
this.iconOnly,
|
|
127
129
|
() => nothing,
|
|
@@ -44,10 +44,10 @@ export default css`
|
|
|
44
44
|
z-index: 0;
|
|
45
45
|
flex: 1;
|
|
46
46
|
border-radius: var(--_state-layer-shape);
|
|
47
|
-
|
|
48
|
-
--md-ripple-hover-
|
|
49
|
-
--md-ripple-
|
|
50
|
-
--md-ripple-pressed-
|
|
47
|
+
|
|
48
|
+
--md-ripple-hover-state-layer-color: var(--md-sys-color-primary);
|
|
49
|
+
--md-ripple-focus-state-layer-color: var(--md-sys-color-primary);
|
|
50
|
+
--md-ripple-pressed-state-layer-color: var(--md-sys-color-on-surface);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
.icon-button:hover {
|
|
@@ -69,8 +69,9 @@ export default css`
|
|
|
69
69
|
--md-focus-ring-shape-end-start: var(--_state-layer-shape);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
.ripple {
|
|
73
73
|
border-radius: var(--_state-layer-shape);
|
|
74
|
+
z-index: 1;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
.icon {
|
|
@@ -79,12 +80,6 @@ export default css`
|
|
|
79
80
|
overflow: hidden;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
.touch {
|
|
83
|
-
position: absolute;
|
|
84
|
-
height: max(48px, 100%);
|
|
85
|
-
width: max(48px, 100%);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
83
|
.user-icon,
|
|
89
84
|
.avatar-initials {
|
|
90
85
|
background-color: var(--md-sys-color-primary-container, #9eeffe);
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
import { html, TemplateResult,
|
|
2
|
-
import { property, state } from 'lit/decorators.js'
|
|
1
|
+
import { html, TemplateResult, PropertyValues, SVGTemplateResult, svg } from 'lit'
|
|
2
|
+
import { property, query, state } from 'lit/decorators.js'
|
|
3
|
+
import { UiElement } from '../../../md/UiElement.js'
|
|
4
|
+
import { setDisabled } from '../../../lib/disabled.js'
|
|
3
5
|
import type { IUser } from '@api-client/core/models/store/User.js'
|
|
4
|
-
import '
|
|
5
|
-
import '
|
|
6
|
+
import type UiRipple from '../../../md/ripple/internals/ripple.js'
|
|
7
|
+
import type { BeginPressConfig, EndPressConfig } from '../../../controllers/ActionController.js'
|
|
8
|
+
import '../../../md/focus-ring/ui-focus-ring.js'
|
|
9
|
+
import '../../../md/ripple/ui-ripple.js'
|
|
6
10
|
|
|
7
11
|
export type AvatarType = 'button' | 'icon'
|
|
8
12
|
|
|
9
|
-
export default class UserAvatar extends
|
|
13
|
+
export default class UserAvatar extends UiElement {
|
|
14
|
+
@query('ui-ripple') accessor ripple: UiRipple | null = null
|
|
15
|
+
/**
|
|
16
|
+
* Whether the avatar is disabled.
|
|
17
|
+
* @attribute
|
|
18
|
+
*/
|
|
19
|
+
@property({ type: Boolean, reflect: true }) accessor disabled = false
|
|
10
20
|
/**
|
|
11
21
|
* Set with the user. The computed user initials.
|
|
22
|
+
* @attribute
|
|
12
23
|
*/
|
|
13
24
|
@state() protected accessor userInitials: string | undefined
|
|
14
25
|
|
|
15
26
|
/**
|
|
16
27
|
* The URL to the user picture.
|
|
28
|
+
* @attribute
|
|
17
29
|
*/
|
|
18
30
|
@state() protected accessor userPicture: string | undefined
|
|
19
31
|
|
|
@@ -27,13 +39,102 @@ export default class UserAvatar extends LitElement {
|
|
|
27
39
|
*/
|
|
28
40
|
@property({ type: String }) accessor type: AvatarType = 'button'
|
|
29
41
|
|
|
42
|
+
constructor() {
|
|
43
|
+
super()
|
|
44
|
+
|
|
45
|
+
this.addEventListener('keydown', this.handleKeyDown.bind(this))
|
|
46
|
+
this.addEventListener('keyup', this.handleKeyUp.bind(this))
|
|
47
|
+
this.addEventListener('pointerenter', this.handlePointerEnter.bind(this))
|
|
48
|
+
this.addEventListener('pointerleave', this.handlePointerLeave.bind(this))
|
|
49
|
+
this.addEventListener('click', this.handleClick.bind(this))
|
|
50
|
+
this.addEventListener('pointerdown', this.handlePointerDown.bind(this))
|
|
51
|
+
this.addEventListener('pointerup', this.handlePointerUp.bind(this))
|
|
52
|
+
this.addEventListener('pointercancel', this.handlePointerCancel.bind(this))
|
|
53
|
+
this.addEventListener('contextmenu', this.handleContextMenu.bind(this))
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
protected override willUpdate(cp: PropertyValues<this>): void {
|
|
31
57
|
if (cp.has('user')) {
|
|
32
58
|
this.handleUserChange(this.user)
|
|
33
59
|
}
|
|
60
|
+
if (cp.has('disabled')) {
|
|
61
|
+
setDisabled(this, cp.get('disabled'))
|
|
62
|
+
}
|
|
34
63
|
super.willUpdate(cp)
|
|
35
64
|
}
|
|
36
65
|
|
|
66
|
+
override beginPress(options: BeginPressConfig): void {
|
|
67
|
+
if (!this.shouldHandleInteraction()) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
super.beginPress(options)
|
|
71
|
+
this.ripple?.beginPress(options.positionEvent)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override endPress(options: EndPressConfig): void {
|
|
75
|
+
if (!this.shouldHandleInteraction()) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
super.endPress(options)
|
|
79
|
+
this.ripple?.endPress()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override handleKeyDown(e: KeyboardEvent): void {
|
|
83
|
+
if (!this.shouldHandleInteraction()) {
|
|
84
|
+
e.stopPropagation()
|
|
85
|
+
e.preventDefault()
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
if (e.code === 'Tab' || e.shiftKey) return
|
|
89
|
+
// do not prevent default, so that parent elements can handle the key event
|
|
90
|
+
this.beginPress({ positionEvent: e })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override handleKeyUp(e: KeyboardEvent): void {
|
|
94
|
+
if (!this.shouldHandleInteraction()) {
|
|
95
|
+
e.stopPropagation()
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// do not prevent default, so that parent elements can handle the key event
|
|
101
|
+
this.endPress({ cancelled: false, actionData: { item: this } })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override handlePointerEnter(e: PointerEvent): void {
|
|
105
|
+
if (!this.shouldHandleInteraction()) {
|
|
106
|
+
e.stopPropagation()
|
|
107
|
+
e.preventDefault()
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
this.ripple?.beginHover(e)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override handlePointerLeave(e: PointerEvent): void {
|
|
114
|
+
if (!this.shouldHandleInteraction()) {
|
|
115
|
+
e.stopPropagation()
|
|
116
|
+
e.preventDefault()
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
super.handlePointerLeave(e)
|
|
120
|
+
|
|
121
|
+
this.ripple?.endHover()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override handleClick(e: MouseEvent): void {
|
|
125
|
+
super.handleClick(e)
|
|
126
|
+
if (this.disabled) {
|
|
127
|
+
e.preventDefault()
|
|
128
|
+
e.stopPropagation()
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
this.endPress({ cancelled: false, actionData: { event: e } })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected shouldHandleInteraction(): boolean {
|
|
135
|
+
return !this.disabled && this.type === 'button'
|
|
136
|
+
}
|
|
137
|
+
|
|
37
138
|
/**
|
|
38
139
|
* Handles changes to the user property.
|
|
39
140
|
* @param user The user object
|
|
@@ -88,10 +189,8 @@ export default class UserAvatar extends LitElement {
|
|
|
88
189
|
protected renderButton(content: TemplateResult): TemplateResult {
|
|
89
190
|
return html`
|
|
90
191
|
<button id="button" class="icon-button">
|
|
91
|
-
|
|
92
|
-
<md-ripple></md-ripple>
|
|
192
|
+
${this.renderFocusRing()} ${this.renderRipple()}
|
|
93
193
|
<span role="presentation" class="icon">${content}</span>
|
|
94
|
-
<span class="touch"></span>
|
|
95
194
|
</button>
|
|
96
195
|
`
|
|
97
196
|
}
|
|
@@ -140,4 +239,12 @@ export default class UserAvatar extends LitElement {
|
|
|
140
239
|
</svg>
|
|
141
240
|
`
|
|
142
241
|
}
|
|
242
|
+
|
|
243
|
+
protected renderRipple(): TemplateResult {
|
|
244
|
+
return html`<ui-ripple class="ripple" ?disabled="${this.disabled}"></ui-ripple>`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
protected renderFocusRing(): TemplateResult {
|
|
248
|
+
return html`<ui-focus-ring part="focus-ring" for="button"></ui-focus-ring>`
|
|
249
|
+
}
|
|
143
250
|
}
|
|
@@ -8,7 +8,7 @@ import type UiRipple from '../../ripple/internals/ripple.js'
|
|
|
8
8
|
import { findElementInShadowRoots } from '../../../lib/Dom.js'
|
|
9
9
|
|
|
10
10
|
import '../../ripple/ui-ripple.js'
|
|
11
|
-
import '
|
|
11
|
+
import '../../focus-ring/ui-focus-ring.js'
|
|
12
12
|
|
|
13
13
|
export type ButtonType = 'submit' | 'reset' | 'button'
|
|
14
14
|
export type MdButtonShape = 'round' | 'square'
|
|
@@ -370,7 +370,7 @@ export default class BaseButton extends UiElement {
|
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
protected renderFocusRing(): TemplateResult {
|
|
373
|
-
return html`<
|
|
373
|
+
return html`<ui-focus-ring part="focus-ring" class="focus-ring" .control="${this as HTMLElement}"></ui-focus-ring>`
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
protected renderRipple(): TemplateResult {
|
|
@@ -7,7 +7,7 @@ import type UiRipple from '../../ripple/internals/ripple.js'
|
|
|
7
7
|
import type { BeginPressConfig, EndPressConfig } from '../../../controllers/ActionController.js'
|
|
8
8
|
|
|
9
9
|
import '../../ripple/ui-ripple.js'
|
|
10
|
-
import '
|
|
10
|
+
import '../../focus-ring/ui-focus-ring.js'
|
|
11
11
|
|
|
12
12
|
export default class CheckboxElement extends CheckedElement {
|
|
13
13
|
protected get _icon(): SVGTemplateResult | typeof nothing {
|
|
@@ -79,7 +79,7 @@ export default class CheckboxElement extends CheckedElement {
|
|
|
79
79
|
pressed,
|
|
80
80
|
})
|
|
81
81
|
return html`
|
|
82
|
-
<
|
|
82
|
+
<ui-focus-ring part="focus-ring" .control="${this as HTMLElement}"></ui-focus-ring>
|
|
83
83
|
<div class="${containerClasses}">
|
|
84
84
|
<div class="container"></div>
|
|
85
85
|
<div class="state"></div>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { css } from 'lit'
|
|
2
|
+
|
|
3
|
+
export default css`
|
|
4
|
+
:host {
|
|
5
|
+
animation-delay: 0s, calc(var(--md-focus-ring-duration, 600ms) * 0.25);
|
|
6
|
+
animation-duration:
|
|
7
|
+
calc(var(--md-focus-ring-duration, 600ms) * 0.25), calc(var(--md-focus-ring-duration, 600ms) * 0.75);
|
|
8
|
+
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
color: var(--md-focus-ring-color, var(--md-sys-color-secondary, #625b71));
|
|
11
|
+
display: none;
|
|
12
|
+
pointer-events: none;
|
|
13
|
+
position: absolute;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:host([visible]) {
|
|
17
|
+
display: flex;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
:host(:not([inward])) {
|
|
21
|
+
animation-name: outward-grow, outward-shrink;
|
|
22
|
+
border-end-end-radius: calc(
|
|
23
|
+
var(--md-focus-ring-shape-end-end, var(--md-focus-ring-shape, var(--md-sys-shape-corner-full, 9999px))) +
|
|
24
|
+
var(--md-focus-ring-outward-offset, 2px)
|
|
25
|
+
);
|
|
26
|
+
border-end-start-radius: calc(
|
|
27
|
+
var(--md-focus-ring-shape-end-start, var(--md-focus-ring-shape, var(--md-sys-shape-corner-full, 9999px))) +
|
|
28
|
+
var(--md-focus-ring-outward-offset, 2px)
|
|
29
|
+
);
|
|
30
|
+
border-start-end-radius: calc(
|
|
31
|
+
var(--md-focus-ring-shape-start-end, var(--md-focus-ring-shape, var(--md-sys-shape-corner-full, 9999px))) +
|
|
32
|
+
var(--md-focus-ring-outward-offset, 2px)
|
|
33
|
+
);
|
|
34
|
+
border-start-start-radius: calc(
|
|
35
|
+
var(--md-focus-ring-shape-start-start, var(--md-focus-ring-shape, var(--md-sys-shape-corner-full, 9999px))) +
|
|
36
|
+
var(--md-focus-ring-outward-offset, 2px)
|
|
37
|
+
);
|
|
38
|
+
inset: calc(-1 * var(--md-focus-ring-outward-offset, 2px));
|
|
39
|
+
outline: var(--md-focus-ring-width, 3px) solid currentColor;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:host([inward]) {
|
|
43
|
+
animation-name: inward-grow, inward-shrink;
|
|
44
|
+
border-end-end-radius: calc(
|
|
45
|
+
var(--md-focus-ring-shape-end-end, var(--md-focus-ring-shape, var(--md-sys-shape-corner-full, 9999px))) - var(
|
|
46
|
+
--md-focus-ring-inward-offset,
|
|
47
|
+
0px
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
border-end-start-radius: calc(
|
|
51
|
+
var(--md-focus-ring-shape-end-start, var(--md-focus-ring-shape, var(--md-sys-shape-corner-full, 9999px))) - var(
|
|
52
|
+
--md-focus-ring-inward-offset,
|
|
53
|
+
0px
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
border-start-end-radius: calc(
|
|
57
|
+
var(--md-focus-ring-shape-start-end, var(--md-focus-ring-shape, var(--md-sys-shape-corner-full, 9999px))) - var(
|
|
58
|
+
--md-focus-ring-inward-offset,
|
|
59
|
+
0px
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
border-start-start-radius: calc(
|
|
63
|
+
var(--md-focus-ring-shape-start-start, var(--md-focus-ring-shape, var(--md-sys-shape-corner-full, 9999px))) - var(
|
|
64
|
+
--md-focus-ring-inward-offset,
|
|
65
|
+
0px
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
border: var(--md-focus-ring-width, 3px) solid currentColor;
|
|
69
|
+
inset: var(--md-focus-ring-inward-offset, 0px);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@keyframes outward-grow {
|
|
73
|
+
from {
|
|
74
|
+
outline-width: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
to {
|
|
78
|
+
outline-width: var(--md-focus-ring-active-width, 8px);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@keyframes outward-shrink {
|
|
83
|
+
from {
|
|
84
|
+
outline-width: var(--md-focus-ring-active-width, 8px);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@keyframes inward-grow {
|
|
89
|
+
from {
|
|
90
|
+
border-width: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
to {
|
|
94
|
+
border-width: var(--md-focus-ring-active-width, 8px);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes inward-shrink {
|
|
99
|
+
from {
|
|
100
|
+
border-width: var(--md-focus-ring-active-width, 8px);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@media (prefers-reduced-motion) {
|
|
105
|
+
:host {
|
|
106
|
+
animation: none;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit'
|
|
2
|
+
import { property } from 'lit/decorators.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A focus ring component that provides a visible focus indicator.
|
|
6
|
+
* This component automatically manages the visibility of the focus ring
|
|
7
|
+
* based on the focus state of its control element.
|
|
8
|
+
*
|
|
9
|
+
* @fires visibility-changed - Dispatched when the focus ring visibility changes.
|
|
10
|
+
* - `detail`: An object with a `visible` property (boolean).
|
|
11
|
+
*/
|
|
12
|
+
export default class UiFocusRing extends LitElement {
|
|
13
|
+
/**
|
|
14
|
+
* The control element that this focus ring is associated with.
|
|
15
|
+
* When the control gains or loses focus, the focus ring will show or hide.
|
|
16
|
+
*/
|
|
17
|
+
@property({ type: Object }) accessor control: HTMLElement | undefined
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An element to attach the focus ring to. Defaults to the parent element.
|
|
21
|
+
* This is typically used when the focus ring should be positioned relative
|
|
22
|
+
* to a different element than the control.
|
|
23
|
+
*/
|
|
24
|
+
@property({ type: Object }) accessor attach: HTMLElement | undefined
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An ID of an element that this focus ring is for.
|
|
28
|
+
* Alternative to setting the `control` property directly.
|
|
29
|
+
* @attribute
|
|
30
|
+
*/
|
|
31
|
+
@property({ type: String }) accessor for: string | undefined
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether the focus ring should render inward instead of outward.
|
|
35
|
+
* When true, the focus ring will be positioned inside the control bounds.
|
|
36
|
+
* @attribute
|
|
37
|
+
*/
|
|
38
|
+
@property({ type: Boolean, reflect: true }) accessor inward = false
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The internal visibility state of the focus ring.
|
|
42
|
+
*/
|
|
43
|
+
@property({ type: Boolean, reflect: true }) accessor visible = false
|
|
44
|
+
|
|
45
|
+
private abortController?: AbortController
|
|
46
|
+
|
|
47
|
+
constructor() {
|
|
48
|
+
super()
|
|
49
|
+
this.addEventListener('visibility-changed', this.onVisibilityChanged as EventListener)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override connectedCallback(): void {
|
|
53
|
+
super.connectedCallback()
|
|
54
|
+
this.setupControl()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override disconnectedCallback(): void {
|
|
58
|
+
super.disconnectedCallback()
|
|
59
|
+
this.cleanupListeners()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected override willUpdate(changedProperties: PropertyValues<this>): void {
|
|
63
|
+
if (changedProperties.has('for') || changedProperties.has('control')) {
|
|
64
|
+
this.setupControl()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sets up the control element and event listeners.
|
|
70
|
+
*/
|
|
71
|
+
private setupControl(): void {
|
|
72
|
+
this.cleanupListeners()
|
|
73
|
+
|
|
74
|
+
const control = this.getControl()
|
|
75
|
+
if (!control) return
|
|
76
|
+
|
|
77
|
+
this.abortController = new AbortController()
|
|
78
|
+
const { signal } = this.abortController
|
|
79
|
+
|
|
80
|
+
// Listen for focus and blur events
|
|
81
|
+
control.addEventListener('focus', this.handleFocus, { signal })
|
|
82
|
+
control.addEventListener('blur', this.handleBlur, { signal })
|
|
83
|
+
control.addEventListener('pointerdown', this.handlePointerDown, { signal })
|
|
84
|
+
|
|
85
|
+
// Set initial state
|
|
86
|
+
this.visible = control.matches(':focus-visible')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Cleans up event listeners.
|
|
91
|
+
*/
|
|
92
|
+
private cleanupListeners(): void {
|
|
93
|
+
this.abortController?.abort()
|
|
94
|
+
this.abortController = undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Gets the control element from either the `control` property,
|
|
99
|
+
* the `for` attribute, or defaults to the parent element.
|
|
100
|
+
*/
|
|
101
|
+
private getControl(): HTMLElement | null {
|
|
102
|
+
if (this.control) {
|
|
103
|
+
return this.control
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (this.for) {
|
|
107
|
+
const root = this.getRootNode() as Document | ShadowRoot
|
|
108
|
+
return root.getElementById(this.for)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return this.parentElement
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Handles focus events from the control element.
|
|
116
|
+
*/
|
|
117
|
+
private handleFocus = (event: FocusEvent): void => {
|
|
118
|
+
// Only show focus ring for keyboard navigation
|
|
119
|
+
if (this.shouldShowFocusRing(event)) {
|
|
120
|
+
this.show()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handles blur events from the control element.
|
|
126
|
+
*/
|
|
127
|
+
private handleBlur = (): void => {
|
|
128
|
+
this.hide()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Handles pointer down events to hide focus ring during mouse interaction.
|
|
133
|
+
*/
|
|
134
|
+
private handlePointerDown = (): void => {
|
|
135
|
+
this.hide()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Determines if the focus ring should be shown based on the focus event.
|
|
140
|
+
* The focus ring should only show for keyboard navigation, not mouse clicks.
|
|
141
|
+
*/
|
|
142
|
+
private shouldShowFocusRing(event: FocusEvent): boolean {
|
|
143
|
+
const control = event.target as HTMLElement
|
|
144
|
+
return control?.matches(':focus-visible') ?? false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Shows the focus ring.
|
|
149
|
+
*/
|
|
150
|
+
show(): void {
|
|
151
|
+
if (this.visible) return
|
|
152
|
+
this.visible = true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Hides the focus ring.
|
|
157
|
+
*/
|
|
158
|
+
hide(): void {
|
|
159
|
+
if (!this.visible) return
|
|
160
|
+
this.visible = false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handles the visibility changed event.
|
|
165
|
+
*/
|
|
166
|
+
private onVisibilityChanged = (event: Event): void => {
|
|
167
|
+
// Stop propagation to prevent multiple focus rings from interfering
|
|
168
|
+
event.stopPropagation()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected override updated(changedProperties: PropertyValues<this>): void {
|
|
172
|
+
super.updated(changedProperties)
|
|
173
|
+
|
|
174
|
+
if (changedProperties.has('visible')) {
|
|
175
|
+
this.dispatchEvent(
|
|
176
|
+
new CustomEvent('visibility-changed', {
|
|
177
|
+
detail: { visible: this.visible },
|
|
178
|
+
bubbles: false,
|
|
179
|
+
composed: true,
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CSSResultOrNative } from 'lit'
|
|
2
|
+
import { customElement } from 'lit/decorators.js'
|
|
3
|
+
import Element from './internals/focus-ring.js'
|
|
4
|
+
import styles from './internals/focus-ring.styles.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A focus ring component that provides a visible focus indicator.
|
|
8
|
+
*
|
|
9
|
+
* This component replaces the Material Design `md-focus-ring` component
|
|
10
|
+
* and provides the same functionality with customizable styling.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```html
|
|
14
|
+
* <!-- Basic usage -->
|
|
15
|
+
* <ui-focus-ring></ui-focus-ring>
|
|
16
|
+
*
|
|
17
|
+
* <!-- With specific control -->
|
|
18
|
+
* <ui-focus-ring .control="${buttonElement}"></ui-focus-ring>
|
|
19
|
+
*
|
|
20
|
+
* <!-- Using for attribute -->
|
|
21
|
+
* <ui-focus-ring for="my-button"></ui-focus-ring>
|
|
22
|
+
*
|
|
23
|
+
* <!-- Inward focus ring -->
|
|
24
|
+
* <ui-focus-ring inward></ui-focus-ring>
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @cssprop --ui-focus-ring-color - The color of the focus ring border.
|
|
28
|
+
* @cssprop --ui-focus-ring-width - The width of the focus ring border.
|
|
29
|
+
* @cssprop --ui-focus-ring-style - The style of the focus ring border (solid, dashed, etc.).
|
|
30
|
+
* @cssprop --ui-focus-ring-shape-start-start - The start-start corner radius.
|
|
31
|
+
* @cssprop --ui-focus-ring-shape-start-end - The start-end corner radius.
|
|
32
|
+
* @cssprop --ui-focus-ring-shape-end-end - The end-end corner radius.
|
|
33
|
+
* @cssprop --ui-focus-ring-shape-end-start - The end-start corner radius.
|
|
34
|
+
*
|
|
35
|
+
* @csspart focus-ring - The focus ring element.
|
|
36
|
+
*/
|
|
37
|
+
@customElement('ui-focus-ring')
|
|
38
|
+
export class UiFocusRingElement extends Element {
|
|
39
|
+
static override styles: CSSResultOrNative[] = [styles]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare global {
|
|
43
|
+
interface HTMLElementTagNameMap {
|
|
44
|
+
'ui-focus-ring': Element
|
|
45
|
+
}
|
|
46
|
+
}
|