@operato/context 10.18.0 → 10.19.1
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/CHANGELOG.md +17 -0
- package/coverage/lcov-report/index.html +52 -37
- package/coverage/lcov.info +10836 -10257
- package/dist/src/tools/ox-page-action-context-bar.js +28 -3
- package/dist/src/tools/ox-page-action-context-bar.js.map +1 -1
- package/dist/test/action-bar-focus.test.d.ts +1 -0
- package/dist/test/action-bar-focus.test.js +67 -0
- package/dist/test/action-bar-focus.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/action-bar-focus.test.ts +83 -0
|
@@ -30,17 +30,43 @@ let PageActionContextBar = class PageActionContextBar extends LitElement {
|
|
|
30
30
|
text-transform: capitalize;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/*
|
|
34
|
+
* ── 키보드로 온 사람에게 지금 어디인지 보인다 ──────────────────────
|
|
35
|
+
* 여기 있던 것은 :host *:focus { outline: none } 한 줄이다. 그런데 이 줄의 단추는 자기
|
|
36
|
+
* 스타일에 이미 초점 표시를 갖고 있었다(ButtonContainerStyles 의 button &:focus-visible,
|
|
37
|
+
* 2px). **그 한 줄이 그것을 이겼다** — :host *:focus 는 특정도 (0,2,0) 이고
|
|
38
|
+
* button:focus-visible 은 (0,1,1) 이다. 그래서 선언은 있는데 화면에는 아무것도 안 그려졌다.
|
|
39
|
+
*
|
|
40
|
+
* [확인함] 2026-09-19 :3500 — 이 줄의 단추에 초점을 주고 재니 outline-style: none 이었다.
|
|
41
|
+
* (그 재기는 :focus 까지만 확인한다. 키보드 초점은 합성으로 못 만든다.)
|
|
42
|
+
*
|
|
43
|
+
* 마우스로 누를 때 테두리가 남는 것이 싫어서 껐던 것은 맞다. 그 뜻은 :focus-visible 이
|
|
44
|
+
* 정확히 낸다. 그래서 끄는 것은 **그 밖의 초점에만** 건다.
|
|
45
|
+
*
|
|
46
|
+
* 굵기는 3px 로 맞춘다 — Material 단추의 링이 3px 이고, 한 줄에 우리 단추와 md 단추가 함께
|
|
47
|
+
* 서므로 두 표시가 같아야 한다.
|
|
48
|
+
*/
|
|
49
|
+
:host *:focus:not(:focus-visible) {
|
|
34
50
|
outline: none;
|
|
35
51
|
}
|
|
36
52
|
|
|
53
|
+
:host *:focus-visible {
|
|
54
|
+
outline: 3px solid var(--md-sys-color-primary);
|
|
55
|
+
outline-offset: 2px;
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
[container] {
|
|
38
59
|
flex: 1;
|
|
39
60
|
|
|
40
61
|
display: flex;
|
|
41
62
|
overflow: hidden;
|
|
42
63
|
gap: var(--spacing-small);
|
|
43
|
-
|
|
64
|
+
/*
|
|
65
|
+
* 초점 링이 설 자리 — 링은 단추 **밖으로 5px** 를 쓴다(3px 굵기 + 2px 띄움). 2px 이던 때는
|
|
66
|
+
* 이 줄이 overflow: hidden 이라 위아래가 잘렸다. 가로로 자르는 것은 남겨 둔다 —
|
|
67
|
+
* 「더보기」가 scrollWidth > clientWidth 로 넘침을 재는데 그 값이 살아 있어야 한다.
|
|
68
|
+
*/
|
|
69
|
+
padding: 5px;
|
|
44
70
|
align-items: center;
|
|
45
71
|
}
|
|
46
72
|
|
|
@@ -90,7 +116,6 @@ let PageActionContextBar = class PageActionContextBar extends LitElement {
|
|
|
90
116
|
color: var(--md-sys-color-on-primary);
|
|
91
117
|
background-color: var(--md-sys-color-on-primary);
|
|
92
118
|
font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
|
|
93
|
-
outline: none;
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
md-icon[working] {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ox-page-action-context-bar.js","sourceRoot":"","sources":["../../../src/tools/ox-page-action-context-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport './ox-page-action-overlay-template.js'\n\nimport { css, html, LitElement, nothing, PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport throttle from 'lodash-es/throttle.js'\nimport { i18next } from '@operato/i18n'\nimport { openOverlay } from '@operato/layout'\nimport { RouteController } from '@operato/shell'\nimport { sleep } from '@operato/utils'\nimport { ButtonContainerStyles } from '@operato/styles'\nimport { formElementHeight } from '@operato/styles/form-element-heights.js'\n\n@customElement('ox-page-action-context-bar')\nexport class PageActionContextBar extends LitElement {\n static get styles() {\n return [\n ButtonContainerStyles,\n css`\n :host {\n display: flex;\n position: relative;\n overflow-x: clip;\n overflow-y: hidden;\n\n text-transform: capitalize;\n }\n\n :host *:focus {\n outline: none;\n }\n\n [container] {\n flex: 1;\n\n display: flex;\n overflow: hidden;\n gap: var(--spacing-small);\n padding: 2px; /* only for button shadow */\n align-items: center;\n }\n\n /*\n * The buttons end where the screen's content ends (ADR-0060 decision 9 addendum).\n *\n * This bar spans the window. A document screen's content does not - it is held to the\n * reading measure - so on a wide window the reader finished the page at one x and the\n * buttons sat far to the right of it. Measured on the approval screen at 1714px: the\n * content ended at 1050 and the buttons began at 1385, 335px away.\n *\n * A work surface fills the window, so its buttons already share its right edge and nothing\n * changes for it. The screen says which it is through context.frame; with nothing said,\n * this stays as it was.\n */\n :host([frame='document']) [container] {\n max-width: var(--ui-measure, min(1080px, 100%));\n margin-right: auto;\n }\n\n /*\n * 액션바 위에 떠 있는 오버플로 버튼 — 배경과 구분되도록 **테마와 무관하게 어두운 표면**을 유지한다.\n *\n * 이전에는 --md-sys-color-inverse-surface 를 썼는데, inverse-* 는 스펙상 \"현재 테마의 반대\"라\n * 라이트에서는 어둡지만 다크에서 밝게 반전되어 눈에 튀었다(ox-snack-bar 와 같은 부류).\n * 반투명 어두운 값을 기본으로 두면 브랜드 팔레트와 무관하게 안정적이고 .dark 패치도 불필요하다.\n */\n [more] {\n position: absolute;\n bottom: 0;\n right: 0;\n color: var(--ox-context-more-color, rgba(255, 255, 255, 0.95));\n background-color: var(--ox-context-more-background, rgba(0, 0, 0, 0.85));\n box-shadow: var(--ox-context-more-shadow, -2px 0px 2px 1px rgba(0, 0, 0, 0.2));\n padding: var(--context-action-bar-more-button-padding, 11px 10px);\n font-size: 2rem;\n z-index: 10;\n }\n\n select,\n input[type='text'] {\n box-sizing: border-box;\n padding: 0 var(--spacing-small);\n height: ${formElementHeight('--form-element-height-medium')};\n border: 1px solid var(--md-sys-color-outline);\n border-radius: var(--md-sys-shape-corner-small);\n color: var(--md-sys-color-on-primary);\n background-color: var(--md-sys-color-on-primary);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n outline: none;\n }\n\n md-icon[working] {\n animation: rotate 1s linear infinite;\n }\n\n @media screen and (max-width: 460px) {\n [container] {\n padding: 0;\n gap: 0;\n }\n\n /* 아주 좁은 화면 — 버튼을 붙여 한 덩어리로 만든다(사이 여백이 자리를 먹는다). */\n button {\n border-radius: 0;\n }\n\n button:not(:last-child) {\n border-right-width: 0;\n }\n }\n\n @keyframes rotate {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n `\n ]\n }\n\n private _route = new RouteController(this)\n\n @property({ type: Array }) actions: {\n title: string\n action: () => void\n icon: string\n type: string\n select: string[]\n href?: string\n emphasis?: boolean\n }[] = []\n\n @state() private overflown: boolean = false\n @query('[container]') container?: HTMLDivElement\n\n private overflownUpdaterThrottle: any\n private resizeCallback: any\n\n get overflownUpdater() {\n if (!this.overflownUpdaterThrottle) {\n this.overflownUpdaterThrottle = throttle(() => {\n this.overflown = !!this.container && this.container.scrollWidth > this.container.clientWidth\n }, 100)\n }\n\n return this.overflownUpdaterThrottle\n }\n\n connectedCallback() {\n super.connectedCallback()\n\n this.resizeCallback = () => this.overflownUpdater()\n\n window.addEventListener('resize', this.resizeCallback)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n\n window.removeEventListener('resize', this.resizeCallback)\n }\n\n render() {\n return this.actions.length > 0\n ? html`\n <div container>${this.renderActions()}</div>\n <!--\n 넘친 동작들에 닿는 **유일한 길**이다. 그런데 이름이 없어 낭독기는 「more_horiz」를\n 읽었고, md-icon 이라 키보드 초점도 못 받았다 — 키보드만 쓰는 사람에게는 그 동작들이\n 아예 없는 것과 같았다.\n -->\n ${this.overflown\n ? html`\n <md-icon\n more\n role=\"button\"\n tabindex=\"0\"\n title=${i18next.t('text.more-actions', { defaultValue: 'more actions' })}\n @click=${this.showMore.bind(this)}\n @keydown=${(e: KeyboardEvent) => {\n if (e.key !== 'Enter' && e.key !== ' ') return\n e.preventDefault()\n this.showMore()\n }}\n >more_horiz</md-icon\n >\n `\n : html``}\n `\n : nothing\n }\n\n renderActions() {\n return html`\n ${this.actions.map(($action, idx) => {\n let { type, title, icon, action, select, href, emphasis, disabled, disabledTitle } = $action as any\n let { raised, danger } = emphasis || {}\n\n return type == 'text'\n ? html` <span @click=${action}>${title}</span> `\n : type == 'select' || (select && select.length > 0)\n ? html`\n <select @change=${action}>\n ${select.map((option: any) => html` <option>${option}</option> `)}\n </select>\n `\n : type == 'link'\n ? html` <a href=${href}>${title}</a> `\n : html`\n <button\n ?icon-only=${type == 'icon' || !title}\n ?raised=${raised}\n ?danger=${danger}\n ?disabled=${disabled}\n title=${disabled && disabledTitle ? disabledTitle : title || ''}\n @click=${async (e: MouseEvent) => {\n /* all actions should be bloked for a second */\n const button = e.currentTarget as any\n const iconElement = button.querySelector('md-icon')\n const actionConfig = this.actions[idx]\n\n if (iconElement) {\n icon = actionConfig.icon\n actionConfig.icon = 'refresh'\n this.requestUpdate()\n iconElement.setAttribute('working', true)\n }\n button.disabled = true\n\n try {\n await action()\n } finally {\n await sleep(1000) /* waiting for additionla 1 second */\n\n /* because icon can be changed after sleep, don't use closure 'icon'. */\n if (iconElement) {\n actionConfig.icon = icon\n this.requestUpdate()\n iconElement.removeAttribute('working')\n }\n button.disabled = false\n }\n }}\n >\n ${this.actions[idx].icon ? html`<md-icon>${this.actions[idx].icon}</md-icon>` : nothing}\n ${type == 'icon' || !title ? nothing : title}\n </button>\n `\n })}\n `\n }\n\n async updated(changed: PropertyValues<this>) {\n if (changed.has('actions')) {\n requestAnimationFrame(this.overflownUpdater)\n }\n }\n\n protected willUpdate() {\n const { actions, frame } = this._route.context || {}\n /* The bar has no box of its own to measure; the attribute is what the stylesheet reads. */\n if (frame) this.setAttribute('frame', String(frame))\n else this.removeAttribute('frame')\n\n if (actions) {\n this.actions = actions.filter((action: any) => !!action)\n }\n }\n\n showMore() {\n openOverlay('context-toolbar-overlay', {\n template: html`<ox-page-action-overlay-template></ox-page-action-overlay-template>`\n })\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ox-page-action-context-bar.js","sourceRoot":"","sources":["../../../src/tools/ox-page-action-context-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport './ox-page-action-overlay-template.js'\n\nimport { css, html, LitElement, nothing, PropertyValues } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport throttle from 'lodash-es/throttle.js'\nimport { i18next } from '@operato/i18n'\nimport { openOverlay } from '@operato/layout'\nimport { RouteController } from '@operato/shell'\nimport { sleep } from '@operato/utils'\nimport { ButtonContainerStyles } from '@operato/styles'\nimport { formElementHeight } from '@operato/styles/form-element-heights.js'\n\n@customElement('ox-page-action-context-bar')\nexport class PageActionContextBar extends LitElement {\n static get styles() {\n return [\n ButtonContainerStyles,\n css`\n :host {\n display: flex;\n position: relative;\n overflow-x: clip;\n overflow-y: hidden;\n\n text-transform: capitalize;\n }\n\n /*\n * ── 키보드로 온 사람에게 지금 어디인지 보인다 ──────────────────────\n * 여기 있던 것은 :host *:focus { outline: none } 한 줄이다. 그런데 이 줄의 단추는 자기\n * 스타일에 이미 초점 표시를 갖고 있었다(ButtonContainerStyles 의 button &:focus-visible,\n * 2px). **그 한 줄이 그것을 이겼다** — :host *:focus 는 특정도 (0,2,0) 이고\n * button:focus-visible 은 (0,1,1) 이다. 그래서 선언은 있는데 화면에는 아무것도 안 그려졌다.\n *\n * [확인함] 2026-09-19 :3500 — 이 줄의 단추에 초점을 주고 재니 outline-style: none 이었다.\n * (그 재기는 :focus 까지만 확인한다. 키보드 초점은 합성으로 못 만든다.)\n *\n * 마우스로 누를 때 테두리가 남는 것이 싫어서 껐던 것은 맞다. 그 뜻은 :focus-visible 이\n * 정확히 낸다. 그래서 끄는 것은 **그 밖의 초점에만** 건다.\n *\n * 굵기는 3px 로 맞춘다 — Material 단추의 링이 3px 이고, 한 줄에 우리 단추와 md 단추가 함께\n * 서므로 두 표시가 같아야 한다.\n */\n :host *:focus:not(:focus-visible) {\n outline: none;\n }\n\n :host *:focus-visible {\n outline: 3px solid var(--md-sys-color-primary);\n outline-offset: 2px;\n }\n\n [container] {\n flex: 1;\n\n display: flex;\n overflow: hidden;\n gap: var(--spacing-small);\n /*\n * 초점 링이 설 자리 — 링은 단추 **밖으로 5px** 를 쓴다(3px 굵기 + 2px 띄움). 2px 이던 때는\n * 이 줄이 overflow: hidden 이라 위아래가 잘렸다. 가로로 자르는 것은 남겨 둔다 —\n * 「더보기」가 scrollWidth > clientWidth 로 넘침을 재는데 그 값이 살아 있어야 한다.\n */\n padding: 5px;\n align-items: center;\n }\n\n /*\n * The buttons end where the screen's content ends (ADR-0060 decision 9 addendum).\n *\n * This bar spans the window. A document screen's content does not - it is held to the\n * reading measure - so on a wide window the reader finished the page at one x and the\n * buttons sat far to the right of it. Measured on the approval screen at 1714px: the\n * content ended at 1050 and the buttons began at 1385, 335px away.\n *\n * A work surface fills the window, so its buttons already share its right edge and nothing\n * changes for it. The screen says which it is through context.frame; with nothing said,\n * this stays as it was.\n */\n :host([frame='document']) [container] {\n max-width: var(--ui-measure, min(1080px, 100%));\n margin-right: auto;\n }\n\n /*\n * 액션바 위에 떠 있는 오버플로 버튼 — 배경과 구분되도록 **테마와 무관하게 어두운 표면**을 유지한다.\n *\n * 이전에는 --md-sys-color-inverse-surface 를 썼는데, inverse-* 는 스펙상 \"현재 테마의 반대\"라\n * 라이트에서는 어둡지만 다크에서 밝게 반전되어 눈에 튀었다(ox-snack-bar 와 같은 부류).\n * 반투명 어두운 값을 기본으로 두면 브랜드 팔레트와 무관하게 안정적이고 .dark 패치도 불필요하다.\n */\n [more] {\n position: absolute;\n bottom: 0;\n right: 0;\n color: var(--ox-context-more-color, rgba(255, 255, 255, 0.95));\n background-color: var(--ox-context-more-background, rgba(0, 0, 0, 0.85));\n box-shadow: var(--ox-context-more-shadow, -2px 0px 2px 1px rgba(0, 0, 0, 0.2));\n padding: var(--context-action-bar-more-button-padding, 11px 10px);\n font-size: 2rem;\n z-index: 10;\n }\n\n select,\n input[type='text'] {\n box-sizing: border-box;\n padding: 0 var(--spacing-small);\n height: ${formElementHeight('--form-element-height-medium')};\n border: 1px solid var(--md-sys-color-outline);\n border-radius: var(--md-sys-shape-corner-small);\n color: var(--md-sys-color-on-primary);\n background-color: var(--md-sys-color-on-primary);\n font-size: var(--md-sys-typescale-label-large-size, 0.875rem);\n }\n\n md-icon[working] {\n animation: rotate 1s linear infinite;\n }\n\n @media screen and (max-width: 460px) {\n [container] {\n padding: 0;\n gap: 0;\n }\n\n /* 아주 좁은 화면 — 버튼을 붙여 한 덩어리로 만든다(사이 여백이 자리를 먹는다). */\n button {\n border-radius: 0;\n }\n\n button:not(:last-child) {\n border-right-width: 0;\n }\n }\n\n @keyframes rotate {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n `\n ]\n }\n\n private _route = new RouteController(this)\n\n @property({ type: Array }) actions: {\n title: string\n action: () => void\n icon: string\n type: string\n select: string[]\n href?: string\n emphasis?: boolean\n }[] = []\n\n @state() private overflown: boolean = false\n @query('[container]') container?: HTMLDivElement\n\n private overflownUpdaterThrottle: any\n private resizeCallback: any\n\n get overflownUpdater() {\n if (!this.overflownUpdaterThrottle) {\n this.overflownUpdaterThrottle = throttle(() => {\n this.overflown = !!this.container && this.container.scrollWidth > this.container.clientWidth\n }, 100)\n }\n\n return this.overflownUpdaterThrottle\n }\n\n connectedCallback() {\n super.connectedCallback()\n\n this.resizeCallback = () => this.overflownUpdater()\n\n window.addEventListener('resize', this.resizeCallback)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n\n window.removeEventListener('resize', this.resizeCallback)\n }\n\n render() {\n return this.actions.length > 0\n ? html`\n <div container>${this.renderActions()}</div>\n <!--\n 넘친 동작들에 닿는 **유일한 길**이다. 그런데 이름이 없어 낭독기는 「more_horiz」를\n 읽었고, md-icon 이라 키보드 초점도 못 받았다 — 키보드만 쓰는 사람에게는 그 동작들이\n 아예 없는 것과 같았다.\n -->\n ${this.overflown\n ? html`\n <md-icon\n more\n role=\"button\"\n tabindex=\"0\"\n title=${i18next.t('text.more-actions', { defaultValue: 'more actions' })}\n @click=${this.showMore.bind(this)}\n @keydown=${(e: KeyboardEvent) => {\n if (e.key !== 'Enter' && e.key !== ' ') return\n e.preventDefault()\n this.showMore()\n }}\n >more_horiz</md-icon\n >\n `\n : html``}\n `\n : nothing\n }\n\n renderActions() {\n return html`\n ${this.actions.map(($action, idx) => {\n let { type, title, icon, action, select, href, emphasis, disabled, disabledTitle } = $action as any\n let { raised, danger } = emphasis || {}\n\n return type == 'text'\n ? html` <span @click=${action}>${title}</span> `\n : type == 'select' || (select && select.length > 0)\n ? html`\n <select @change=${action}>\n ${select.map((option: any) => html` <option>${option}</option> `)}\n </select>\n `\n : type == 'link'\n ? html` <a href=${href}>${title}</a> `\n : html`\n <button\n ?icon-only=${type == 'icon' || !title}\n ?raised=${raised}\n ?danger=${danger}\n ?disabled=${disabled}\n title=${disabled && disabledTitle ? disabledTitle : title || ''}\n @click=${async (e: MouseEvent) => {\n /* all actions should be bloked for a second */\n const button = e.currentTarget as any\n const iconElement = button.querySelector('md-icon')\n const actionConfig = this.actions[idx]\n\n if (iconElement) {\n icon = actionConfig.icon\n actionConfig.icon = 'refresh'\n this.requestUpdate()\n iconElement.setAttribute('working', true)\n }\n button.disabled = true\n\n try {\n await action()\n } finally {\n await sleep(1000) /* waiting for additionla 1 second */\n\n /* because icon can be changed after sleep, don't use closure 'icon'. */\n if (iconElement) {\n actionConfig.icon = icon\n this.requestUpdate()\n iconElement.removeAttribute('working')\n }\n button.disabled = false\n }\n }}\n >\n ${this.actions[idx].icon ? html`<md-icon>${this.actions[idx].icon}</md-icon>` : nothing}\n ${type == 'icon' || !title ? nothing : title}\n </button>\n `\n })}\n `\n }\n\n async updated(changed: PropertyValues<this>) {\n if (changed.has('actions')) {\n requestAnimationFrame(this.overflownUpdater)\n }\n }\n\n protected willUpdate() {\n const { actions, frame } = this._route.context || {}\n /* The bar has no box of its own to measure; the attribute is what the stylesheet reads. */\n if (frame) this.setAttribute('frame', String(frame))\n else this.removeAttribute('frame')\n\n if (actions) {\n this.actions = actions.filter((action: any) => !!action)\n }\n }\n\n showMore() {\n openOverlay('context-toolbar-overlay', {\n template: html`<ox-page-action-overlay-template></ox-page-action-overlay-template>`\n })\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../src/tools/ox-page-action-context-bar.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* 요소끼리 비교하지 않는다 — 실패하면 chai 가 DOM 을 펼치다 러너가 멈춘다. */
|
|
2
|
+
import { expect, fixture } from '@open-wc/testing';
|
|
3
|
+
import { html } from 'lit';
|
|
4
|
+
import '../src/tools/ox-page-action-context-bar.js';
|
|
5
|
+
/**
|
|
6
|
+
* 키보드로 온 사람이 **지금 어디인지 보인다.**
|
|
7
|
+
*
|
|
8
|
+
* ── 무엇이 있었나 ─────────────────────────────────────────────────────────
|
|
9
|
+
* 이 줄의 스타일에 `:host *:focus { outline: none }` 한 줄이 있었고 대신 그릴 것이 없었다. 그래서
|
|
10
|
+
* **하단 동작 줄의 단추에는 초점 표시가 아예 없었다** — 모든 제품의 모든 화면에 이 줄이 선다.
|
|
11
|
+
* [확인함] 2026-09-19 :3500 — 단추에 초점을 주고 재니 `outline-style: none` · `box-shadow: none`.
|
|
12
|
+
*
|
|
13
|
+
* 마우스로 누를 때 테두리가 뜨는 것이 싫어서 지운 것은 맞다. 그 뜻은 `:focus-visible` 이 정확히 낸다.
|
|
14
|
+
*
|
|
15
|
+
* ── 자리도 함께 준다 ──────────────────────────────────────────────────────
|
|
16
|
+
* 링은 단추 **밖으로 5px** 를 쓴다(3px 굵기 + 2px 띄움). 담는 줄이 `overflow: hidden` 이고 여백이
|
|
17
|
+
* 2px 이던 때는 그려도 위아래가 잘렸다. 그래서 이 시험은 **그렸나**와 **설 자리가 있나**를 함께 본다.
|
|
18
|
+
*/
|
|
19
|
+
const ACTIONS = [
|
|
20
|
+
{ title: 'save', action: () => { } },
|
|
21
|
+
{ title: 'cancel', action: () => { } }
|
|
22
|
+
];
|
|
23
|
+
async function bar() {
|
|
24
|
+
const el = (await fixture(html `
|
|
25
|
+
<ox-page-action-context-bar .actions=${ACTIONS}></ox-page-action-context-bar>
|
|
26
|
+
`));
|
|
27
|
+
await el.updateComplete;
|
|
28
|
+
return el;
|
|
29
|
+
}
|
|
30
|
+
const buttonOf = (el) => el.renderRoot.querySelector('button');
|
|
31
|
+
describe('하단 동작 줄의 초점 표시', () => {
|
|
32
|
+
it('키보드로 온 초점에 테두리를 그린다', async () => {
|
|
33
|
+
const el = await bar();
|
|
34
|
+
expect(buttonOf(el), '단추가 없다').to.not.equal(null);
|
|
35
|
+
/*
|
|
36
|
+
* 브라우저는 **키를 누른 뒤의 초점**만 키보드 초점으로 본다. 합성 키는 그림자 DOM 을 못 지나므로
|
|
37
|
+
* (2026-09-18 에 그것으로 없는 결함을 보고했다) 여기서는 규칙이 서 있는지를 본다. 실제로 그려지는
|
|
38
|
+
* 것은 앱에서 잰다.
|
|
39
|
+
*
|
|
40
|
+
* **주석을 걷고 본다.** 안 그러면 이 파일이 설명으로 적은 옛 규칙이 검사를 만족시킨다 —
|
|
41
|
+
* 자기 산문을 세는 것이 이 집 시험의 오래된 함정이다.
|
|
42
|
+
*/
|
|
43
|
+
const rules = Array.from(el.constructor.elementStyles)
|
|
44
|
+
.map((sheet) => { var _a; return String((_a = sheet.cssText) !== null && _a !== void 0 ? _a : ''); })
|
|
45
|
+
.join('\n')
|
|
46
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
47
|
+
/* 큰 문자열을 match 에 넣으면 실패 출력이 화면을 덮는다 — 참·거짓만 본다. */
|
|
48
|
+
expect(/:focus-visible\s*\{[^}]*outline:\s*3px solid/.test(rules), 'focus-visible 에 그릴 것이 없다').to.equal(true);
|
|
49
|
+
expect(/\*:focus\s*\{\s*outline:\s*none/.test(rules), '초점을 통째로 끄는 줄이 남아 있다').to.equal(false);
|
|
50
|
+
expect(/:focus:not\(:focus-visible\)\s*\{[^}]*outline:\s*none/.test(rules), '마우스 초점을 끄는 줄이 없다 — 누를 때마다 테두리가 뜬다').to.equal(true);
|
|
51
|
+
});
|
|
52
|
+
it('링이 설 자리가 있다 — 그려도 잘리면 없는 것과 같다', async () => {
|
|
53
|
+
const el = await bar();
|
|
54
|
+
const container = el.renderRoot.querySelector('[container]');
|
|
55
|
+
const style = getComputedStyle(container);
|
|
56
|
+
/* 링은 밖으로 5px 를 쓴다: 3px 굵기 + 2px 띄움. */
|
|
57
|
+
expect(parseFloat(style.paddingTop), '위 여백이 링보다 좁다').to.be.at.least(5);
|
|
58
|
+
expect(parseFloat(style.paddingBottom), '아래 여백이 링보다 좁다').to.be.at.least(5);
|
|
59
|
+
expect(parseFloat(style.paddingLeft), '왼쪽 여백이 링보다 좁다').to.be.at.least(5);
|
|
60
|
+
});
|
|
61
|
+
it('가로로 자르는 것은 그대로다 — 「더보기」가 넘침을 그 값으로 잰다', async () => {
|
|
62
|
+
const el = await bar();
|
|
63
|
+
const container = el.renderRoot.querySelector('[container]');
|
|
64
|
+
expect(getComputedStyle(container).overflowX).to.equal('hidden');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
//# sourceMappingURL=action-bar-focus.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-bar-focus.test.js","sourceRoot":"","sources":["../../test/action-bar-focus.test.ts"],"names":[],"mappings":"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","sourcesContent":["/* 요소끼리 비교하지 않는다 — 실패하면 chai 가 DOM 을 펼치다 러너가 멈춘다. */\nimport { expect, fixture } from '@open-wc/testing'\nimport { html } from 'lit'\n\nimport '../src/tools/ox-page-action-context-bar.js'\n\n/**\n * 키보드로 온 사람이 **지금 어디인지 보인다.**\n *\n * ── 무엇이 있었나 ─────────────────────────────────────────────────────────\n * 이 줄의 스타일에 `:host *:focus { outline: none }` 한 줄이 있었고 대신 그릴 것이 없었다. 그래서\n * **하단 동작 줄의 단추에는 초점 표시가 아예 없었다** — 모든 제품의 모든 화면에 이 줄이 선다.\n * [확인함] 2026-09-19 :3500 — 단추에 초점을 주고 재니 `outline-style: none` · `box-shadow: none`.\n *\n * 마우스로 누를 때 테두리가 뜨는 것이 싫어서 지운 것은 맞다. 그 뜻은 `:focus-visible` 이 정확히 낸다.\n *\n * ── 자리도 함께 준다 ──────────────────────────────────────────────────────\n * 링은 단추 **밖으로 5px** 를 쓴다(3px 굵기 + 2px 띄움). 담는 줄이 `overflow: hidden` 이고 여백이\n * 2px 이던 때는 그려도 위아래가 잘렸다. 그래서 이 시험은 **그렸나**와 **설 자리가 있나**를 함께 본다.\n */\n\nconst ACTIONS = [\n { title: 'save', action: () => {} },\n { title: 'cancel', action: () => {} }\n]\n\nasync function bar() {\n const el = (await fixture(html`\n <ox-page-action-context-bar .actions=${ACTIONS}></ox-page-action-context-bar>\n `)) as HTMLElement & { renderRoot: ShadowRoot; updateComplete: Promise<unknown> }\n\n await el.updateComplete\n return el\n}\n\nconst buttonOf = (el: HTMLElement & { renderRoot: ShadowRoot }) => el.renderRoot.querySelector('button') as HTMLElement\n\ndescribe('하단 동작 줄의 초점 표시', () => {\n it('키보드로 온 초점에 테두리를 그린다', async () => {\n const el = await bar()\n\n expect(buttonOf(el), '단추가 없다').to.not.equal(null)\n\n /*\n * 브라우저는 **키를 누른 뒤의 초점**만 키보드 초점으로 본다. 합성 키는 그림자 DOM 을 못 지나므로\n * (2026-09-18 에 그것으로 없는 결함을 보고했다) 여기서는 규칙이 서 있는지를 본다. 실제로 그려지는\n * 것은 앱에서 잰다.\n *\n * **주석을 걷고 본다.** 안 그러면 이 파일이 설명으로 적은 옛 규칙이 검사를 만족시킨다 —\n * 자기 산문을 세는 것이 이 집 시험의 오래된 함정이다.\n */\n const rules = Array.from((el.constructor as any).elementStyles)\n .map((sheet: any) => String(sheet.cssText ?? ''))\n .join('\\n')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, ' ')\n\n /* 큰 문자열을 match 에 넣으면 실패 출력이 화면을 덮는다 — 참·거짓만 본다. */\n expect(/:focus-visible\\s*\\{[^}]*outline:\\s*3px solid/.test(rules), 'focus-visible 에 그릴 것이 없다').to.equal(true)\n expect(/\\*:focus\\s*\\{\\s*outline:\\s*none/.test(rules), '초점을 통째로 끄는 줄이 남아 있다').to.equal(false)\n expect(\n /:focus:not\\(:focus-visible\\)\\s*\\{[^}]*outline:\\s*none/.test(rules),\n '마우스 초점을 끄는 줄이 없다 — 누를 때마다 테두리가 뜬다'\n ).to.equal(true)\n })\n\n it('링이 설 자리가 있다 — 그려도 잘리면 없는 것과 같다', async () => {\n const el = await bar()\n const container = el.renderRoot.querySelector('[container]') as HTMLElement\n const style = getComputedStyle(container)\n\n /* 링은 밖으로 5px 를 쓴다: 3px 굵기 + 2px 띄움. */\n expect(parseFloat(style.paddingTop), '위 여백이 링보다 좁다').to.be.at.least(5)\n expect(parseFloat(style.paddingBottom), '아래 여백이 링보다 좁다').to.be.at.least(5)\n expect(parseFloat(style.paddingLeft), '왼쪽 여백이 링보다 좁다').to.be.at.least(5)\n })\n\n it('가로로 자르는 것은 그대로다 — 「더보기」가 넘침을 그 값으로 잰다', async () => {\n const el = await bar()\n const container = el.renderRoot.querySelector('[container]') as HTMLElement\n\n expect(getComputedStyle(container).overflowX).to.equal('hidden')\n })\n})\n"]}
|