-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: 仕様書に基づいてコンポーネントを再設計 #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,76 @@ | ||||||||||||||||||||||||||||||||||||
| import { consume } from '@lit/context' | ||||||||||||||||||||||||||||||||||||
| import { css, html, LitElement, nothing } from 'lit' | ||||||||||||||||||||||||||||||||||||
| import { customElement } from 'lit/decorators.js' | ||||||||||||||||||||||||||||||||||||
| import { authContext } from '../../../contexts/auth-context.js' | ||||||||||||||||||||||||||||||||||||
| import { RepositoryObserver } from '../../../controllers/RepositoryObserver.js' | ||||||||||||||||||||||||||||||||||||
| import type { IAuthRepository } from '../../../domain/AuthRepository.js' | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * A declarative component that protects its content based on authentication status. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| @customElement('me-auth-guard') | ||||||||||||||||||||||||||||||||||||
| export class MeAuthGuard extends LitElement { | ||||||||||||||||||||||||||||||||||||
| @consume({ context: authContext, subscribe: true }) | ||||||||||||||||||||||||||||||||||||
| set authRepo(repo: IAuthRepository) { | ||||||||||||||||||||||||||||||||||||
| if (this._authRepo === repo) return | ||||||||||||||||||||||||||||||||||||
| this._authRepo = repo | ||||||||||||||||||||||||||||||||||||
| this._observer?.disconnect() | ||||||||||||||||||||||||||||||||||||
| if (repo) this._observer = new RepositoryObserver(this, repo) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
この setter は observer を張り替えるだけなので、新しい repo が 修正案 set authRepo(repo: IAuthRepository) {
if (this._authRepo === repo) return
this._authRepo = repo
this._observer?.disconnect()
- if (repo) this._observer = new RepositoryObserver(this, repo)
+ if (repo) {
+ this._observer = new RepositoryObserver(this, repo)
+ void this.checkSession()
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| get authRepo() { | ||||||||||||||||||||||||||||||||||||
| return this._authRepo | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| private _authRepo!: IAuthRepository | ||||||||||||||||||||||||||||||||||||
| private _observer?: RepositoryObserver | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| connectedCallback() { | ||||||||||||||||||||||||||||||||||||
| super.connectedCallback() | ||||||||||||||||||||||||||||||||||||
| this.checkSession() | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private async checkSession() { | ||||||||||||||||||||||||||||||||||||
| if (this.authRepo?.status === 'unknown') { | ||||||||||||||||||||||||||||||||||||
| await this.authRepo.refreshSession() | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| render() { | ||||||||||||||||||||||||||||||||||||
| const status = this.authRepo?.status | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (status === 'checking' || status === 'unknown') { | ||||||||||||||||||||||||||||||||||||
| return html` | ||||||||||||||||||||||||||||||||||||
| <div class="guard-status"> | ||||||||||||||||||||||||||||||||||||
| <p>認証状態を確認しています...</p> | ||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||
| ` | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (status === 'authenticated') { | ||||||||||||||||||||||||||||||||||||
| return html`<slot></slot>` | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Guest or failed - render nothing, parent orchestrator will handle redirect | ||||||||||||||||||||||||||||||||||||
| return nothing | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| static styles = css` | ||||||||||||||||||||||||||||||||||||
| :host { | ||||||||||||||||||||||||||||||||||||
| display: contents; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| .guard-status { | ||||||||||||||||||||||||||||||||||||
| min-height: 60dvh; | ||||||||||||||||||||||||||||||||||||
| display: grid; | ||||||||||||||||||||||||||||||||||||
| place-items: center; | ||||||||||||||||||||||||||||||||||||
| color: var(--color-text-secondary); | ||||||||||||||||||||||||||||||||||||
| font-size: 15px; | ||||||||||||||||||||||||||||||||||||
| letter-spacing: var(--tracking-wide); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| ` | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| declare global { | ||||||||||||||||||||||||||||||||||||
| interface HTMLElementTagNameMap { | ||||||||||||||||||||||||||||||||||||
| 'me-auth-guard': MeAuthGuard | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,15 @@ | ||
| import { css, html, LitElement } from 'lit' | ||
| import { customElement, property } from 'lit/decorators.js' | ||
| import { classMap } from 'lit/directives/class-map.js' | ||
|
|
||
| @customElement('me-select') | ||
| export class MeSelect extends LitElement { | ||
| static formAssociated = true | ||
|
|
||
| @property() label = '' | ||
| @property() name = '' | ||
| @property() value = '' | ||
| @property({ type: Boolean }) disabled = false | ||
| @property({ type: Boolean }) required = false | ||
| @property({ reflect: true }) label = '' | ||
| @property({ reflect: true }) name = '' | ||
| @property({ reflect: true }) value = '' | ||
| @property({ type: Boolean, reflect: true }) disabled = false | ||
| @property({ type: Boolean, reflect: true }) required = false | ||
|
|
||
| private _internals: ElementInternals | ||
| private _inputId = `me-input-${Math.random().toString(36).slice(2, 9)}` | ||
|
|
@@ -20,9 +19,13 @@ export class MeSelect extends LitElement { | |
| this._internals = this.attachInternals() | ||
| } | ||
|
|
||
| protected createRenderRoot() { | ||
| return this.attachShadow({ mode: 'open', delegatesFocus: true }) | ||
| } | ||
|
|
||
| formResetCallback() { | ||
| this.value = '' | ||
| this._internals.setFormValue('') | ||
| this._syncInternals() | ||
| } | ||
|
Comment on lines
26
to
29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 27 で常に 🤖 Prompt for AI Agents |
||
|
|
||
| formDisabledCallback(disabled: boolean) { | ||
|
|
@@ -32,7 +35,7 @@ export class MeSelect extends LitElement { | |
| private _onChange(e: Event) { | ||
| const select = e.target as HTMLSelectElement | ||
| this.value = select.value | ||
| this._internals.setFormValue(select.value) | ||
| this._syncInternals() | ||
|
|
||
| this.dispatchEvent( | ||
| new CustomEvent('change', { | ||
|
|
@@ -43,23 +46,38 @@ export class MeSelect extends LitElement { | |
| ) | ||
| } | ||
|
|
||
| updated(changedProperties: Map<PropertyKey, unknown>) { | ||
| protected updated(changedProperties: Map<PropertyKey, unknown>) { | ||
| if (changedProperties.has('value')) { | ||
| this._internals.setFormValue(this.value ?? '') | ||
| this._syncInternals() | ||
| } | ||
| } | ||
|
|
||
| private _syncInternals() { | ||
| this._internals.setFormValue(this.value ?? '') | ||
| const select = this.shadowRoot?.querySelector('select') | ||
| if (select) { | ||
| this._internals.setValidity( | ||
| select.validity, | ||
| select.validationMessage, | ||
| select, | ||
| ) | ||
| this._internals.ariaInvalid = select.checkValidity() ? 'false' : 'true' | ||
| this._internals.ariaRequired = this.required ? 'true' : 'false' | ||
| } | ||
| } | ||
|
|
||
| render() { | ||
| return html` | ||
| <div class=${classMap({ field: true, disabled: this.disabled })}> | ||
| <div class="field"> | ||
| ${ | ||
| this.label | ||
| ? html`<label class="label" for=${this._inputId}>${this.label}</label>` | ||
| ? html`<label class="label" for=${this._inputId} part="label">${this.label}</label>` | ||
| : null | ||
| } | ||
| <div class="select-wrapper"> | ||
| <select | ||
| id=${this._inputId} | ||
| part="select" | ||
| .name=${this.name} | ||
| .value=${this.value ?? ''} | ||
| ?disabled=${this.disabled} | ||
|
|
@@ -130,7 +148,7 @@ export class MeSelect extends LitElement { | |
| box-shadow: 0 0 0 1px var(--admin-accent); | ||
| } | ||
|
|
||
| select:disabled { | ||
| :host([disabled]) select { | ||
| background: var(--color-bg-deep); | ||
| color: var(--color-text-tertiary); | ||
| cursor: not-allowed; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { customElement, property } from 'lit/decorators.js' | |
| /** | ||
| * A standard-compliant, form-associated text input component. | ||
| * Fully leverages ElementInternals and delegatesFocus for a native feel. | ||
| * Targets WCAG 2.1 AA compliance. | ||
| */ | ||
| @customElement('me-text-input') | ||
| export class MeTextInput extends LitElement { | ||
|
|
@@ -34,11 +35,6 @@ export class MeTextInput extends LitElement { | |
| this._internals = this.attachInternals() | ||
| } | ||
|
|
||
| /** | ||
| * Overrides createRenderRoot to enable focus delegation. | ||
| * This means focusing the <me-text-input> element will automatically | ||
| * focus the inner <input> tag. | ||
| */ | ||
| protected createRenderRoot() { | ||
| return this.attachShadow({ mode: 'open', delegatesFocus: true }) | ||
| } | ||
|
|
@@ -47,29 +43,18 @@ export class MeTextInput extends LitElement { | |
|
|
||
| formResetCallback() { | ||
| this.value = '' | ||
| this._internals.setFormValue('') | ||
| this._syncInternals() | ||
|
Comment on lines
44
to
+46
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 45 で常に 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| formDisabledCallback(disabled: boolean) { | ||
| this.disabled = disabled | ||
| } | ||
|
|
||
| // --- Validation --- | ||
|
|
||
| checkValidity() { | ||
| return this._internals.checkValidity() | ||
| } | ||
|
|
||
| reportValidity() { | ||
| return this._internals.reportValidity() | ||
| } | ||
|
|
||
| private _onInput(e: Event) { | ||
| const input = e.target as HTMLInputElement | ||
| this.value = input.value | ||
| this._syncInternals() | ||
|
|
||
| // Bubbles the event as a standard 'change' event for parent listeners | ||
| this.dispatchEvent( | ||
| new CustomEvent('change', { | ||
| detail: input.value, | ||
|
|
@@ -89,20 +74,25 @@ export class MeTextInput extends LitElement { | |
| const val = String(this.value ?? '') | ||
| this._internals.setFormValue(val) | ||
|
|
||
| // Simple native validation sync | ||
| const input = this.shadowRoot?.querySelector('input') | ||
| if (input) { | ||
| // Synchronize native validation to the component's internal state | ||
| const isValid = input.checkValidity() | ||
| this._internals.setValidity( | ||
| input.validity, | ||
| input.validationMessage, | ||
| input, | ||
| ) | ||
|
|
||
| // Update ARIA attributes via internals | ||
| this._internals.ariaInvalid = isValid ? 'false' : 'true' | ||
| this._internals.ariaRequired = this.required ? 'true' : 'false' | ||
| } | ||
| } | ||
|
|
||
| render() { | ||
| return html` | ||
| <div class="field" ?disabled=${this.disabled}> | ||
| <div class="field"> | ||
| ${ | ||
| this.label | ||
| ? html`<label class="label" for=${this._inputId} part="label">${this.label}</label>` | ||
|
|
@@ -161,7 +151,6 @@ export class MeTextInput extends LitElement { | |
| box-shadow: 0 0 0 1px var(--admin-accent); | ||
| } | ||
|
|
||
| /* Standard states via CSS classes or attributes */ | ||
| :host([disabled]) input { | ||
| background: var(--color-bg-deep); | ||
| color: var(--color-text-tertiary); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,21 +30,13 @@ export class MeTextarea extends LitElement { | |
|
|
||
| formResetCallback() { | ||
| this.value = '' | ||
| this._internals.setFormValue('') | ||
| this._syncInternals() | ||
|
Comment on lines
31
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 32 が空文字固定なので、既定値付きフォームで 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| formDisabledCallback(disabled: boolean) { | ||
| this.disabled = disabled | ||
| } | ||
|
|
||
| checkValidity() { | ||
| return this._internals.checkValidity() | ||
| } | ||
|
|
||
| reportValidity() { | ||
| return this._internals.reportValidity() | ||
| } | ||
|
|
||
| private _onInput(e: Event) { | ||
| const input = e.target as HTMLTextAreaElement | ||
| this.value = input.value | ||
|
|
@@ -74,12 +66,14 @@ export class MeTextarea extends LitElement { | |
| input.validationMessage, | ||
| input, | ||
| ) | ||
| this._internals.ariaInvalid = input.checkValidity() ? 'false' : 'true' | ||
| this._internals.ariaRequired = this.required ? 'true' : 'false' | ||
| } | ||
| } | ||
|
|
||
| render() { | ||
| return html` | ||
| <div class="field" ?disabled=${this.disabled}> | ||
| <div class="field"> | ||
| ${ | ||
| this.label | ||
| ? html`<label class="label" for=${this._inputId} part="label">${this.label}</label>` | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.