Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
004f2382cb | ||
|
|
6cf142e35b |
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [0.27.4] - 2026-09-29
|
||||
### Added
|
||||
- Generic `.bbk-notice` / `.bbk-notice--info` message box and a
|
||||
`--bbk-color-info` custom property in `checkout.css`.
|
||||
- Cart drawer focus handling: focus moves into the drawer on open, stays
|
||||
inside it, returns to the opener on close, and is restored after each
|
||||
cart update. Updates are announced as "Cart updated" instead of re-reading
|
||||
the whole cart. New translation line `checkout.cart.updated` — re-run
|
||||
`CheckoutTranslationsSeeder` in consuming apps to add it.
|
||||
|
||||
### Changed
|
||||
- Cart drawer line: larger remove button on the title row, line total on
|
||||
the quantity-stepper row, and screen-reader labels tied to the product
|
||||
name. `.bbk-cart-item-aside` is removed — hosts restyling it should target
|
||||
`.bbk-cart-item-head` / `.bbk-cart-item-foot` instead.
|
||||
- Order confirmation page: narrower (560px), the confirmation-email note is
|
||||
now an info box under the heading, the order summary comes before
|
||||
shipping/billing (shown side by side), and the order number is prefixed
|
||||
with `#`.
|
||||
|
||||
## [0.27.3] - 2026-09-29
|
||||
### Added
|
||||
- The checkout confirmation page now ends with the store's bank transfer
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "boboko/core",
|
||||
"description": "Core module — authentication and shared panel behaviour",
|
||||
"type": "library",
|
||||
"version": "0.27.3",
|
||||
"version": "0.27.4",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Core\\": "src/"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@boboko/core",
|
||||
"version": "0.27.3",
|
||||
"version": "0.27.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Portable Stimulus controllers and styles for boboko-core's cart + checkout module. Installed as a real npm dependency (file:../boboko-core in dev, a tagged git install in prod) so a consuming app's `npm install` resolves this package's own dependencies (leaflet, @hotwired/stimulus) transitively, the same way `composer update boboko/*` does for PHP. See CONTRIBUTE.md's \"JS/CSS: a real npm package\" section.",
|
||||
|
||||
+85
-30
@@ -53,6 +53,7 @@
|
||||
--bbk-color-accent: #18181b;
|
||||
--bbk-color-accent-text: #ffffff;
|
||||
--bbk-color-danger: #dc2626;
|
||||
--bbk-color-info: #2563eb;
|
||||
|
||||
--bbk-radius: 8px;
|
||||
--bbk-radius-sm: 4px;
|
||||
@@ -153,6 +154,11 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Programmatic focus targets only (tabindex=-1) — never reached by Tab, so
|
||||
no ring needed. */
|
||||
.bbk-cart-heading:focus,
|
||||
.bbk-checkout-summary-heading:focus { outline: none; }
|
||||
|
||||
.bbk-visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -183,7 +189,7 @@
|
||||
|
||||
.bbk-cart-item {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr auto;
|
||||
grid-template-columns: 72px 1fr;
|
||||
gap: 0.875rem;
|
||||
align-items: start;
|
||||
}
|
||||
@@ -199,8 +205,23 @@
|
||||
|
||||
.bbk-cart-item-detail { min-width: 0; }
|
||||
|
||||
/* Title + remove button share the first row; quantity stepper + line total
|
||||
share the last one. */
|
||||
.bbk-cart-item-head,
|
||||
.bbk-cart-item-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.bbk-cart-item-head { align-items: flex-start; }
|
||||
.bbk-cart-item-foot { align-items: center; }
|
||||
|
||||
.bbk-cart-item-remove-form { flex: 0 0 auto; }
|
||||
|
||||
.bbk-cart-item-title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
margin: 0 0 0.25rem;
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
@@ -252,24 +273,24 @@ a.bbk-cart-item-title:hover { text-decoration: underline; }
|
||||
color: var(--bbk-color-muted);
|
||||
}
|
||||
|
||||
.bbk-cart-item-aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.bbk-cart-item-total { margin: 0; font-weight: 600; }
|
||||
.bbk-cart-item-total { margin: 0; font-weight: 600; white-space: nowrap; }
|
||||
|
||||
/* 2.5rem hit area (same as the drawer's own close button), pulled up/right by
|
||||
negative margins so the glyph lines up with the title's first line instead
|
||||
of pushing the row taller. */
|
||||
.bbk-cart-item-remove {
|
||||
font-size: 1.125rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 1.75rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
margin: -0.5rem -0.5rem 0 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--bbk-radius-sm);
|
||||
}
|
||||
|
||||
.bbk-cart-item-remove:hover { background: var(--bbk-color-bg-muted); }
|
||||
|
||||
.bbk-cart-qty {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -905,10 +926,39 @@ textarea.bbk-field-input { resize: vertical; }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ── Notice ────────────────────────────────────────────────────────────
|
||||
Inline, static message box: `.bbk-notice` + a tone modifier. Static
|
||||
content, so no live-region role — for messages injected after load, add
|
||||
role="status" (or role="alert" for errors) on the element itself. */
|
||||
|
||||
.bbk-notice {
|
||||
--bbk-notice-color: var(--bbk-color-text);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
padding: 0.875rem 1rem;
|
||||
border: 1px solid color-mix(in srgb, var(--bbk-notice-color) 25%, transparent);
|
||||
border-radius: var(--bbk-radius-sm);
|
||||
background: color-mix(in srgb, var(--bbk-notice-color) 6%, var(--bbk-color-bg));
|
||||
color: var(--bbk-color-text);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.bbk-notice--info { --bbk-notice-color: var(--bbk-color-info); }
|
||||
|
||||
.bbk-notice-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: var(--bbk-notice-color);
|
||||
}
|
||||
|
||||
.bbk-notice-text { margin: 0; align-self: center; }
|
||||
|
||||
/* ── Confirmation page ─────────────────────────────────────────────── */
|
||||
|
||||
.bbk-confirmation {
|
||||
max-width: 720px;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 1.5rem 5rem;
|
||||
font-family: var(--bbk-font);
|
||||
@@ -916,7 +966,7 @@ textarea.bbk-field-input { resize: vertical; }
|
||||
}
|
||||
|
||||
.bbk-confirmation-heading {
|
||||
margin: 0 0 1rem;
|
||||
margin: 0 0 1.25rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -924,7 +974,7 @@ textarea.bbk-field-input { resize: vertical; }
|
||||
.bbk-confirmation-ref { margin: 0 0 0.25rem; }
|
||||
|
||||
.bbk-confirmation-meta {
|
||||
margin: 0 0 1rem;
|
||||
margin: 1.5rem 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
@@ -940,17 +990,22 @@ textarea.bbk-field-input { resize: vertical; }
|
||||
.bbk-confirmation-meta-row dt { color: var(--bbk-color-muted); }
|
||||
.bbk-confirmation-meta-row dd { margin: 0; font-weight: 600; }
|
||||
|
||||
.bbk-confirmation-body {
|
||||
margin: 2rem 0;
|
||||
display: grid;
|
||||
gap: 2.5rem;
|
||||
.bbk-confirmation-section {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--bbk-color-border);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.bbk-confirmation-body { grid-template-columns: 1fr 1fr; }
|
||||
.bbk-confirmation-section-heading {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bbk-confirmation-lines {
|
||||
list-style: none;
|
||||
margin: 0 0 1.25rem;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
@@ -965,16 +1020,21 @@ textarea.bbk-field-input { resize: vertical; }
|
||||
|
||||
.bbk-confirmation-line-detail { min-width: 0; }
|
||||
|
||||
.bbk-confirmation-line-name { margin: 0 0 0.25rem; }
|
||||
|
||||
.bbk-confirmation-line-total { margin: 0; white-space: nowrap; }
|
||||
|
||||
.bbk-confirmation-line-qty { color: var(--bbk-color-muted); }
|
||||
|
||||
.bbk-confirmation-lines .bbk-cart-summary { margin-top: 0.75rem; }
|
||||
|
||||
.bbk-confirmation-addresses {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.bbk-confirmation-addresses { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
.bbk-confirmation-address-heading,
|
||||
.bbk-confirmation-bank-transfer-heading {
|
||||
margin: 0 0 0.5rem;
|
||||
@@ -982,11 +1042,6 @@ textarea.bbk-field-input { resize: vertical; }
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bbk-confirmation-bank-transfer {
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--bbk-color-border);
|
||||
}
|
||||
|
||||
/* Store-authored rich text (ManageStoreDetails' RichEditor) — may be
|
||||
paragraphs, bold text or a bank/IBAN/BIC table. */
|
||||
.bbk-confirmation-bank-transfer-body {
|
||||
|
||||
@@ -9,11 +9,13 @@ import { csrfToken } from './csrf'
|
||||
// - handles the in-drawer quantity / remove forms (fetch + method spoofing)
|
||||
// - re-emits `bbk-cart:updated` {count, total} after every render so the host
|
||||
// (e.g. the header bag icon) can react
|
||||
// - dialog focus handling: focus moves into the panel on open, Tab is kept
|
||||
// inside it, and focus returns to whatever opened it on close
|
||||
//
|
||||
// Appearance is entirely CSS-driven: open state is the data-bbk-cart-state
|
||||
// attribute on the root, nothing here touches styles or class lists.
|
||||
export default class extends Controller {
|
||||
static targets = ['panel', 'body', 'error']
|
||||
static targets = ['panel', 'body', 'error', 'heading', 'status']
|
||||
|
||||
connect() {
|
||||
this.onChanged = this.onChanged.bind(this)
|
||||
@@ -40,19 +42,31 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
onKeydown(event) {
|
||||
if (event.key === 'Escape' && !this.element.hidden) this.close()
|
||||
// Only the drawer instance is a dialog — the checkout page's summary
|
||||
// reuses this controller without a panel.
|
||||
if (!this.hasPanelTarget || this.element.hidden) return
|
||||
|
||||
if (event.key === 'Escape') this.close()
|
||||
if (event.key === 'Tab') this.trapFocus(event)
|
||||
}
|
||||
|
||||
open() {
|
||||
if (!this.element.hidden) return
|
||||
this.returnFocusTo = document.activeElement
|
||||
this.element.hidden = false
|
||||
// Next frame, so the panel transitions from its off-canvas start.
|
||||
requestAnimationFrame(() => this.element.setAttribute('data-bbk-cart-state', 'open'))
|
||||
requestAnimationFrame(() => {
|
||||
this.element.setAttribute('data-bbk-cart-state', 'open')
|
||||
if (this.hasHeadingTarget) this.headingTarget.focus({ preventScroll: true })
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
this.element.removeAttribute('data-bbk-cart-state')
|
||||
|
||||
if (this.returnFocusTo?.isConnected) this.returnFocusTo.focus({ preventScroll: true })
|
||||
this.returnFocusTo = null
|
||||
|
||||
const panel = this.panelTarget
|
||||
const done = () => {
|
||||
this.element.hidden = true
|
||||
@@ -132,6 +146,28 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// aria-modal hides the page from screen readers but doesn't stop Tab from
|
||||
// walking out of the panel into it — wrap at either end instead.
|
||||
trapFocus(event) {
|
||||
const focusable = [...this.panelTarget.querySelectorAll(
|
||||
'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
||||
)].filter((el) => !el.closest('[hidden], [aria-hidden="true"]'))
|
||||
|
||||
if (!focusable.length) return
|
||||
|
||||
const first = focusable[0]
|
||||
const last = focusable[focusable.length - 1]
|
||||
const active = document.activeElement
|
||||
|
||||
if (event.shiftKey && (active === first || !this.panelTarget.contains(active) || (this.hasHeadingTarget && active === this.headingTarget))) {
|
||||
event.preventDefault()
|
||||
last.focus()
|
||||
} else if (!event.shiftKey && (active === last || !this.panelTarget.contains(active))) {
|
||||
event.preventDefault()
|
||||
first.focus()
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
if (!this.hasErrorTarget || !message) return
|
||||
this.errorTarget.textContent = message
|
||||
@@ -144,10 +180,53 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
replaceBody(html) {
|
||||
const restore = this.focusSnapshot()
|
||||
this.bodyTarget.innerHTML = html
|
||||
restore()
|
||||
this.announce()
|
||||
this.emitUpdated(this.bodyTarget.querySelector('[data-bbk-cart-count]'))
|
||||
}
|
||||
|
||||
// Swapping the body destroys whatever control had focus (a qty stepper,
|
||||
// a remove button, the coupon field), dropping keyboard/screen-reader
|
||||
// users back at the top of the document. Returns a callback that, after
|
||||
// the swap, re-focuses the equivalent control in the new markup — or the
|
||||
// heading, when that control is gone (e.g. its line was just removed).
|
||||
focusSnapshot() {
|
||||
const active = document.activeElement
|
||||
if (!active || !this.bodyTarget.contains(active)) return () => {}
|
||||
|
||||
let selector = null
|
||||
if (active.id) {
|
||||
selector = `#${CSS.escape(active.id)}`
|
||||
} else {
|
||||
const lineId = active.closest('[data-bbk-line-id]')?.dataset.bbkLineId
|
||||
const dir = active.dataset.bbkCartDirParam
|
||||
const control = ['bbk-cart-qty-input', 'bbk-cart-qty-btn', 'bbk-cart-item-remove']
|
||||
.find((name) => active.classList.contains(name))
|
||||
|
||||
if (lineId && control) {
|
||||
selector = `[data-bbk-line-id="${CSS.escape(lineId)}"] .${control}`
|
||||
+ (dir ? `[data-bbk-cart-dir-param="${CSS.escape(dir)}"]` : '')
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
const target = selector && this.bodyTarget.querySelector(selector)
|
||||
if (target) target.focus({ preventScroll: true })
|
||||
else if (this.hasHeadingTarget) this.headingTarget.focus({ preventScroll: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Polite "Cart updated" — cleared first so an identical message is
|
||||
// re-announced on the next update.
|
||||
announce() {
|
||||
if (!this.hasStatusTarget) return
|
||||
const message = this.statusTarget.dataset.bbkCartMessage || ''
|
||||
this.statusTarget.textContent = ''
|
||||
requestAnimationFrame(() => { this.statusTarget.textContent = message })
|
||||
}
|
||||
|
||||
emitUpdated(node) {
|
||||
if (!node) return
|
||||
|
||||
|
||||
@@ -14,10 +14,19 @@
|
||||
<div class="bbk-confirmation">
|
||||
<h1 class="bbk-confirmation-heading">{{ __('checkout.page.confirmation_heading') }}</h1>
|
||||
|
||||
<div class="bbk-notice bbk-notice--info">
|
||||
<svg class="bbk-notice-icon" aria-hidden="true" focusable="false" viewBox="0 0 20 20" width="20" height="20">
|
||||
<circle cx="10" cy="10" r="8.25" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M10 9v5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="10" cy="6.25" r="1" fill="currentColor"/>
|
||||
</svg>
|
||||
<p class="bbk-notice-text">{{ __('checkout.page.confirmation_email_note') }}</p>
|
||||
</div>
|
||||
|
||||
<dl class="bbk-confirmation-meta">
|
||||
<div class="bbk-confirmation-meta-row">
|
||||
<dt>{{ __('checkout.page.confirmation_order_number') }}</dt>
|
||||
<dd>{{ \Modules\Core\Order\Support\OrderReferenceDisplay::resolve($order) }}</dd>
|
||||
<dd>#{{ \Modules\Core\Order\Support\OrderReferenceDisplay::resolve($order) }}</dd>
|
||||
</div>
|
||||
|
||||
@if ($order->billingAddress?->contact_email)
|
||||
@@ -42,8 +51,6 @@
|
||||
@endif
|
||||
</dl>
|
||||
|
||||
<p class="bbk-checkout-note">{{ __('checkout.page.confirmation_email_note') }}</p>
|
||||
|
||||
{{-- Guests: logging in with the order's email attaches it to an account
|
||||
(boboko-core's Modules\Core\Customer\Listeners\ClaimGuestOrdersOnLogin),
|
||||
so it shows in their history. --}}
|
||||
@@ -56,21 +63,29 @@
|
||||
@endif
|
||||
@endguest
|
||||
|
||||
<div class="bbk-confirmation-body">
|
||||
<div class="bbk-confirmation-lines">
|
||||
<section class="bbk-confirmation-section" aria-labelledby="bbk-confirmation-summary-heading">
|
||||
<h2 class="bbk-confirmation-section-heading" id="bbk-confirmation-summary-heading">
|
||||
{{ __('checkout.page.order_summary_heading') }}
|
||||
</h2>
|
||||
|
||||
<ul class="bbk-confirmation-lines">
|
||||
@foreach ($order->lines->where('type', '!=', 'shipping') as $line)
|
||||
<div class="bbk-confirmation-line">
|
||||
<li class="bbk-confirmation-line">
|
||||
<div class="bbk-cart-item-media">
|
||||
{{-- alt="" — the description is right beside it. --}}
|
||||
@if ($thumb = $line->purchasable?->getThumbnailImage())
|
||||
<img src="{{ $thumb }}" alt="{{ $line->description }}" width="72" height="72" loading="lazy">
|
||||
<img src="{{ $thumb }}" alt="" width="72" height="72" loading="lazy">
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="bbk-confirmation-line-detail">
|
||||
<span class="bbk-confirmation-line-name">
|
||||
<p class="bbk-confirmation-line-name">
|
||||
{{ $line->description }}
|
||||
<span class="bbk-confirmation-line-qty">× {{ $line->quantity }}</span>
|
||||
</span>
|
||||
<span class="bbk-confirmation-line-qty">
|
||||
<span aria-hidden="true">× {{ $line->quantity }}</span>
|
||||
<span class="bbk-visually-hidden">— {{ __('checkout.cart.quantity') }}: {{ $line->quantity }}</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@if ($line->option)
|
||||
<p class="bbk-cart-item-variant">{{ $line->option }}</p>
|
||||
@@ -79,62 +94,67 @@
|
||||
@include('checkout::partials.line-custom-fields', ['line' => $line])
|
||||
</div>
|
||||
|
||||
<span class="bbk-confirmation-line-total">{{ $line->sub_total?->formatted() }}</span>
|
||||
</div>
|
||||
<p class="bbk-confirmation-line-total">
|
||||
<span class="bbk-visually-hidden">{{ __('checkout.cart.total') }}:</span>
|
||||
{{ $line->sub_total?->formatted() }}
|
||||
</p>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
<div class="bbk-cart-summary">
|
||||
<div class="bbk-cart-summary">
|
||||
<div class="bbk-cart-summary-row">
|
||||
<span>{{ __('checkout.cart.subtotal') }}</span>
|
||||
<span>{{ $order->sub_total?->formatted() }}</span>
|
||||
</div>
|
||||
|
||||
@if ($order->discount_total?->value > 0)
|
||||
<div class="bbk-cart-summary-row bbk-cart-summary-row--discount">
|
||||
<span>{{ __('checkout.cart.discount') }}</span>
|
||||
<span>−{{ $order->discount_total->formatted() }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bbk-cart-summary-row">
|
||||
<span>{{ __('checkout.cart.shipping') }}</span>
|
||||
<span>{{ $order->shipping_total?->formatted() }}</span>
|
||||
</div>
|
||||
|
||||
@if ($order->tax_total?->value > 0)
|
||||
<div class="bbk-cart-summary-row">
|
||||
<span>{{ __('checkout.cart.subtotal') }}</span>
|
||||
<span>{{ $order->sub_total?->formatted() }}</span>
|
||||
<span>{{ __('checkout.cart.tax') }}</span>
|
||||
<span>{{ $order->tax_total->formatted() }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($order->discount_total?->value > 0)
|
||||
<div class="bbk-cart-summary-row bbk-cart-summary-row--discount">
|
||||
<span>{{ __('checkout.cart.discount') }}</span>
|
||||
<span>−{{ $order->discount_total->formatted() }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bbk-cart-summary-row">
|
||||
<span>{{ __('checkout.cart.shipping') }}</span>
|
||||
<span>{{ $order->shipping_total?->formatted() }}</span>
|
||||
</div>
|
||||
|
||||
@if ($order->tax_total?->value > 0)
|
||||
<div class="bbk-cart-summary-row">
|
||||
<span>{{ __('checkout.cart.tax') }}</span>
|
||||
<span>{{ $order->tax_total->formatted() }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bbk-cart-summary-row bbk-cart-summary-row--total">
|
||||
<span>{{ __('checkout.cart.total') }}</span>
|
||||
<span>{{ $order->total?->formatted() }}</span>
|
||||
</div>
|
||||
<div class="bbk-cart-summary-row bbk-cart-summary-row--total">
|
||||
<span>{{ __('checkout.cart.total') }}</span>
|
||||
<span>{{ $order->total?->formatted() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="bbk-confirmation-addresses">
|
||||
@if ($order->shippingAddress || $order->billingAddress)
|
||||
<div class="bbk-confirmation-section bbk-confirmation-addresses">
|
||||
@if ($order->shippingAddress)
|
||||
<div class="bbk-confirmation-address">
|
||||
<h2 class="bbk-confirmation-address-heading">{{ __('checkout.page.confirmation_shipping_to') }}</h2>
|
||||
<section class="bbk-confirmation-address" aria-labelledby="bbk-confirmation-shipping-heading">
|
||||
<h2 class="bbk-confirmation-address-heading" id="bbk-confirmation-shipping-heading">{{ __('checkout.page.confirmation_shipping_to') }}</h2>
|
||||
<x-checkout::address-lines :address="$order->shippingAddress" />
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if ($order->billingAddress)
|
||||
<div class="bbk-confirmation-address">
|
||||
<h2 class="bbk-confirmation-address-heading">{{ __('checkout.page.confirmation_billing') }}</h2>
|
||||
<section class="bbk-confirmation-address" aria-labelledby="bbk-confirmation-billing-heading">
|
||||
<h2 class="bbk-confirmation-address-heading" id="bbk-confirmation-billing-heading">{{ __('checkout.page.confirmation_billing') }}</h2>
|
||||
<x-checkout::address-lines :address="$order->billingAddress" />
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($bankTransferInstructions)
|
||||
<section class="bbk-confirmation-bank-transfer">
|
||||
<h2 class="bbk-confirmation-bank-transfer-heading">{{ __('checkout.page.confirmation_bank_transfer_heading') }}</h2>
|
||||
<section class="bbk-confirmation-section bbk-confirmation-bank-transfer" aria-labelledby="bbk-confirmation-bank-transfer-heading">
|
||||
<h2 class="bbk-confirmation-bank-transfer-heading" id="bbk-confirmation-bank-transfer-heading">{{ __('checkout.page.confirmation_bank_transfer_heading') }}</h2>
|
||||
<div class="bbk-confirmation-bank-transfer-body">{!! $bankTransferInstructions !!}</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
data-bbk-cart-target="panel"
|
||||
>
|
||||
<header class="bbk-cart-panel-header">
|
||||
<h2 class="bbk-cart-heading" id="bbk-cart-heading">{{ __('checkout.cart.title') }}</h2>
|
||||
{{-- tabindex=-1: the controller moves focus here on open, and back
|
||||
here when the focused line is removed from under the user. --}}
|
||||
<h2 class="bbk-cart-heading" id="bbk-cart-heading" tabindex="-1" data-bbk-cart-target="heading">{{ __('checkout.cart.title') }}</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="bbk-cart-dismiss"
|
||||
@@ -26,7 +28,16 @@
|
||||
|
||||
@include('checkout::partials.cart-error')
|
||||
|
||||
<div class="bbk-cart-panel-body" data-bbk-cart-target="body" aria-live="polite">
|
||||
{{-- A short announcement after each update, rather than aria-live on
|
||||
the body itself, which re-read the whole cart on every change. --}}
|
||||
<p
|
||||
class="bbk-visually-hidden"
|
||||
role="status"
|
||||
data-bbk-cart-target="status"
|
||||
data-bbk-cart-message="{{ __('checkout.cart.updated') }}"
|
||||
></p>
|
||||
|
||||
<div class="bbk-cart-panel-body" data-bbk-cart-target="body">
|
||||
@include('checkout::partials.cart-body')
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -267,9 +267,15 @@
|
||||
|
||||
<aside class="bbk-checkout-aside">
|
||||
<div class="bbk-checkout-summary" data-controller="bbk-cart">
|
||||
<h2 class="bbk-checkout-summary-heading">{{ __('checkout.page.order_summary_heading') }}</h2>
|
||||
<h2 class="bbk-checkout-summary-heading" tabindex="-1" data-bbk-cart-target="heading">{{ __('checkout.page.order_summary_heading') }}</h2>
|
||||
@include('checkout::partials.cart-error')
|
||||
<div data-bbk-cart-target="body" aria-live="polite">
|
||||
<p
|
||||
class="bbk-visually-hidden"
|
||||
role="status"
|
||||
data-bbk-cart-target="status"
|
||||
data-bbk-cart-message="{{ __('checkout.cart.updated') }}"
|
||||
></p>
|
||||
<div data-bbk-cart-target="body">
|
||||
@include('checkout::partials.cart-body')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,80 +24,96 @@
|
||||
<li class="bbk-cart-item" data-bbk-line-id="{{ $line->id }}">
|
||||
<div class="bbk-cart-item-media">
|
||||
@if ($thumb)
|
||||
{{-- Decorative duplicate of the title link below — hidden from AT
|
||||
and skipped by keyboard so the product isn't announced twice. --}}
|
||||
@if ($productUrl)
|
||||
<a href="{{ $productUrl }}" aria-hidden="true" tabindex="-1">
|
||||
<img src="{{ $thumb }}" alt="{{ $name }}" width="72" height="72" loading="lazy">
|
||||
<img src="{{ $thumb }}" alt="" width="72" height="72" loading="lazy">
|
||||
</a>
|
||||
@else
|
||||
<img src="{{ $thumb }}" alt="{{ $name }}" width="72" height="72" loading="lazy">
|
||||
<img src="{{ $thumb }}" alt="" width="72" height="72" loading="lazy">
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="bbk-cart-item-detail">
|
||||
@if ($productUrl)
|
||||
<a href="{{ $productUrl }}" class="bbk-cart-item-title">{{ $name }}</a>
|
||||
@else
|
||||
<p class="bbk-cart-item-title">{{ $name }}</p>
|
||||
@endif
|
||||
<div class="bbk-cart-item-head">
|
||||
@if ($productUrl)
|
||||
<a href="{{ $productUrl }}" class="bbk-cart-item-title" id="bbk-cart-item-title-{{ $line->id }}">{{ $name }}</a>
|
||||
@else
|
||||
<p class="bbk-cart-item-title" id="bbk-cart-item-title-{{ $line->id }}">{{ $name }}</p>
|
||||
@endif
|
||||
|
||||
<form
|
||||
class="bbk-cart-item-remove-form"
|
||||
method="POST"
|
||||
action="{{ route('checkout.cart.remove', ['locale' => app()->getLocale(), 'line' => $line->id]) }}"
|
||||
data-action="submit->bbk-cart#submit"
|
||||
>
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
{{-- Named "Remove", described by the product title, so AT
|
||||
hears which line it removes rather than a bare "Remove". --}}
|
||||
<button
|
||||
type="submit"
|
||||
class="bbk-cart-item-remove"
|
||||
aria-label="{{ __('checkout.cart.remove') }}"
|
||||
aria-describedby="bbk-cart-item-title-{{ $line->id }}"
|
||||
><span aria-hidden="true">×</span></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if ($variantLabel)
|
||||
<p class="bbk-cart-item-variant">{{ $variantLabel }}</p>
|
||||
@endif
|
||||
@include('checkout::partials.line-custom-fields', ['line' => $line])
|
||||
<p class="bbk-cart-item-unit">{{ $line->unitPrice?->formatted() }}</p>
|
||||
|
||||
<form
|
||||
class="bbk-cart-qty"
|
||||
method="POST"
|
||||
action="{{ route('checkout.cart.update', ['locale' => app()->getLocale(), 'line' => $line->id]) }}"
|
||||
>
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<button
|
||||
type="button"
|
||||
class="bbk-cart-qty-btn"
|
||||
data-action="bbk-cart#step"
|
||||
data-bbk-cart-dir-param="-1"
|
||||
aria-label="{{ __('checkout.cart.decrease') }}"
|
||||
>−</button>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
name="quantity"
|
||||
value="{{ $line->quantity }}"
|
||||
min="0"
|
||||
inputmode="numeric"
|
||||
class="bbk-cart-qty-input"
|
||||
data-action="change->bbk-cart#submit"
|
||||
data-bbk-cart-confirmed-quantity="{{ $line->quantity }}"
|
||||
aria-label="{{ __('checkout.cart.quantity') }}"
|
||||
<div class="bbk-cart-item-foot">
|
||||
{{-- role=group + the title as its name: entering the stepper
|
||||
announces which product's quantity is being changed. --}}
|
||||
<form
|
||||
class="bbk-cart-qty"
|
||||
method="POST"
|
||||
action="{{ route('checkout.cart.update', ['locale' => app()->getLocale(), 'line' => $line->id]) }}"
|
||||
role="group"
|
||||
aria-labelledby="bbk-cart-item-title-{{ $line->id }}"
|
||||
>
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<button
|
||||
type="button"
|
||||
class="bbk-cart-qty-btn"
|
||||
data-action="bbk-cart#step"
|
||||
data-bbk-cart-dir-param="-1"
|
||||
aria-label="{{ __('checkout.cart.decrease') }}"
|
||||
><span aria-hidden="true">−</span></button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="bbk-cart-qty-btn"
|
||||
data-action="bbk-cart#step"
|
||||
data-bbk-cart-dir-param="1"
|
||||
aria-label="{{ __('checkout.cart.increase') }}"
|
||||
>+</button>
|
||||
</form>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
name="quantity"
|
||||
value="{{ $line->quantity }}"
|
||||
min="0"
|
||||
inputmode="numeric"
|
||||
class="bbk-cart-qty-input"
|
||||
data-action="change->bbk-cart#submit"
|
||||
data-bbk-cart-confirmed-quantity="{{ $line->quantity }}"
|
||||
aria-label="{{ __('checkout.cart.quantity') }}"
|
||||
>
|
||||
|
||||
<div class="bbk-cart-item-aside">
|
||||
<p class="bbk-cart-item-total">{{ $line->subTotal?->formatted() }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="bbk-cart-qty-btn"
|
||||
data-action="bbk-cart#step"
|
||||
data-bbk-cart-dir-param="1"
|
||||
aria-label="{{ __('checkout.cart.increase') }}"
|
||||
><span aria-hidden="true">+</span></button>
|
||||
</form>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="{{ route('checkout.cart.remove', ['locale' => app()->getLocale(), 'line' => $line->id]) }}"
|
||||
data-action="submit->bbk-cart#submit"
|
||||
>
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button
|
||||
type="submit"
|
||||
class="bbk-cart-item-remove"
|
||||
aria-label="{{ __('checkout.cart.remove') }}"
|
||||
>×</button>
|
||||
</form>
|
||||
<p class="bbk-cart-item-total">
|
||||
<span class="bbk-visually-hidden">{{ __('checkout.cart.total') }}:</span>
|
||||
{{ $line->subTotal?->formatted() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -60,6 +60,7 @@ class CheckoutTranslationsSeeder extends Seeder
|
||||
'cart.increase' => ['Increase quantity', 'Αύξηση ποσότητας'],
|
||||
'cart.decrease' => ['Decrease quantity', 'Μείωση ποσότητας'],
|
||||
'cart.remove' => ['Remove', 'Αφαίρεση'],
|
||||
'cart.updated' => ['Cart updated', 'Το καλάθι ενημερώθηκε'],
|
||||
'cart.subtotal' => ['Subtotal', 'Υποσύνολο'],
|
||||
'cart.discount' => ['Discount', 'Έκπτωση'],
|
||||
'cart.shipping' => ['Shipping', 'Μεταφορικά'],
|
||||
|
||||
Reference in New Issue
Block a user