Skip to content
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

🐛 fix: define segment component earlier #1471

Merged
merged 6 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/giant-buses-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@baloise/ds-core': patch
---

**segment**: resolve custom elemen creation in angular applications
8 changes: 6 additions & 2 deletions packages/angular/src/components/bal-segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
import { NG_VALUE_ACCESSOR } from '@angular/forms'

import type { Components } from '@baloise/ds-core'
import { defineCustomElement } from '@baloise/ds-core/components/bal-segment'
import { defineCustomElement as defineSegment } from '@baloise/ds-core/components/bal-segment'
import { defineCustomElement as defineSegmentItem } from '@baloise/ds-core/components/bal-segment-item'

import { ProxyCmp, proxyOutputs } from '../generated/angular-component-lib/utils'
import { ValueAccessor } from '../generated/value-accessor'
Expand All @@ -25,7 +26,10 @@ const accessorProvider = {
}

@ProxyCmp({
defineCustomElementFn: defineCustomElement,
defineCustomElementFn: () => {
defineSegment()
defineSegmentItem()
},
inputs: BalSegmentInputs,
methods: BalSegmentMethods,
})
Expand Down
26 changes: 7 additions & 19 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2741,6 +2741,7 @@ export namespace Components {
* If `true`, and is vertical then the list height is limited and scrollable.
*/
"scrollable": boolean;
"setAriaForm": (ariaForm: BalAriaForm) => Promise<void>;
/**
* the value of the segment.
*/
Expand All @@ -2765,6 +2766,7 @@ export namespace Components {
* Label of the segment control
*/
"label": string;
"setAriaForm": (ariaForm: BalAriaForm) => Promise<void>;
"setFocus": () => Promise<void>;
/**
* The value of the segment button.
Expand Down Expand Up @@ -3700,10 +3702,6 @@ export interface BalSegmentCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLBalSegmentElement;
}
export interface BalSegmentItemCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLBalSegmentItemElement;
}
export interface BalSelectCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLBalSelectElement;
Expand Down Expand Up @@ -4725,6 +4723,7 @@ declare global {
new (): HTMLBalRadioGroupElement;
};
interface HTMLBalSegmentElementEventMap {
"balFocus": BalEvents.BalSegmentFocusDetail;
"balBlur": BalEvents.BalSegmentBlurDetail;
"balChange": BalEvents.BalSegmentChangeDetail;
"balSelect": BalEvents.BalSegmentChangeDetail;
Expand All @@ -4744,18 +4743,7 @@ declare global {
prototype: HTMLBalSegmentElement;
new (): HTMLBalSegmentElement;
};
interface HTMLBalSegmentItemElementEventMap {
"balBlur": BalEvents.BalSegmentBlurDetail;
}
interface HTMLBalSegmentItemElement extends Components.BalSegmentItem, HTMLStencilElement {
addEventListener<K extends keyof HTMLBalSegmentItemElementEventMap>(type: K, listener: (this: HTMLBalSegmentItemElement, ev: BalSegmentItemCustomEvent<HTMLBalSegmentItemElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLBalSegmentItemElementEventMap>(type: K, listener: (this: HTMLBalSegmentItemElement, ev: BalSegmentItemCustomEvent<HTMLBalSegmentItemElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLBalSegmentItemElement: {
prototype: HTMLBalSegmentItemElement;
Expand Down Expand Up @@ -7915,6 +7903,10 @@ declare namespace LocalJSX {
* Emitted when the value property has changed and any dragging pointer has been released from `bal-segment`. This event will not emit when programmatically setting the `value` property.
*/
"onBalChange"?: (event: BalSegmentCustomEvent<BalEvents.BalSegmentChangeDetail>) => void;
/**
* Emitted when the toggle has focus.
*/
"onBalFocus"?: (event: BalSegmentCustomEvent<BalEvents.BalSegmentFocusDetail>) => void;
/**
* Emitted when the value of the segment changes from user committed actions or from externally assigning a value.
*/
Expand Down Expand Up @@ -7951,10 +7943,6 @@ declare namespace LocalJSX {
* Label of the segment control
*/
"label"?: string;
/**
* Emitted when the component was touched
*/
"onBalBlur"?: (event: BalSegmentItemCustomEvent<BalEvents.BalSegmentBlurDetail>) => void;
/**
* The value of the segment button.
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/components/bal-field/bal-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class Field implements ComponentInterface, BalMutationObserver {
'bal-input-slider',
'bal-file-upload',
'bal-dropdown',
'bal-segment',
]
private formElements = [...this.formControlElement, 'bal-field-label', 'bal-field-message']

Expand Down Expand Up @@ -172,6 +173,8 @@ export class Field implements ComponentInterface, BalMutationObserver {
'bal-field-control bal-dropdown',
'bal-field-control bal-checkbox',
'bal-field-control bal-radio',
'bal-field-control bal-segment-item',
'bal-field-control bal-segment',
'bal-field-control bal-checkbox-group',
'bal-field-control bal-radio-group',
'bal-field-control bal-number-input',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
import {
Component,
h,
ComponentInterface,
Host,
Element,
Prop,
State,
Watch,
Method,
EventEmitter,
Event,
} from '@stencil/core'
import { Component, h, ComponentInterface, Host, Element, Prop, State, Watch, Method } from '@stencil/core'
import { BEM } from '../../../utils/bem'
import { SegmentValue } from '../bal-segment.types'
import { Attributes, inheritAttributes } from '../../../utils/attributes'
import { addEventListener, raf, removeEventListener } from '../../../utils/helpers'
import { BalAriaForm, defaultBalAriaForm } from '../../../utils/form'

let ids = 0
let SegmentItemIds = 0

@Component({
tag: 'bal-segment-item',
Expand All @@ -26,13 +15,18 @@ export class SegmentItem implements ComponentInterface {
private segmentEl: HTMLBalSegmentElement | null = null
private nativeEl: HTMLButtonElement | undefined
private inheritedAttributes: Attributes = {}
private id = ids++
private internalId = SegmentItemIds++
private inputId = `bal-si-${this.internalId}`

@Element() el!: HTMLElement

@State() hasSlotContent = false
@State() isFocusable = false
@State() isVertical = false
@State() isLast = false
@State() isFirst = false
@State() hasEmptyValue = true
@State() ariaForm: BalAriaForm = defaultBalAriaForm

/**
* If `true`, the user cannot interact with the segment button.
Expand Down Expand Up @@ -64,24 +58,31 @@ export class SegmentItem implements ComponentInterface {
/**
* The value of the segment button.
*/
@Prop() value: SegmentValue = 'bal-si-' + this.id
@Prop({ mutable: true }) value: SegmentValue = 'bal-si-' + this.internalId
@Watch('value')
valueChanged() {
this.updateState()
valueChanged(newValue: SegmentValue, oldValue: SegmentValue) {
if (newValue !== oldValue) {
this.updateState()
}
}

/**
* Emitted when the component was touched
*/
@Event() balBlur!: EventEmitter<BalEvents.BalSegmentBlurDetail>
componentWillLoad() {
this.inheritedAttributes = {
...inheritAttributes(this.el, ['aria-label']),
}
}

connectedCallback() {
componentDidLoad() {
const segmentEl = (this.segmentEl = this.el.closest('bal-segment'))
if (segmentEl) {
this.updateState()
addEventListener(segmentEl, 'balSelect', this.updateState)
addEventListener(segmentEl, 'balVertical', this.updateVertical)
}

raf(() => {
this.checkSlotContent()
this.updateState()
})
}

disconnectedCallback() {
Expand All @@ -93,16 +94,15 @@ export class SegmentItem implements ComponentInterface {
}
}

componentWillLoad() {
this.inheritedAttributes = {
...inheritAttributes(this.el, ['aria-label']),
private calculateEmptyValue() {
if (this.segmentEl) {
const segments = Array.from(this.segmentEl.querySelectorAll('bal-segment-item'))
this.hasEmptyValue = !segments.some(item => item.value === this.segmentEl.value)
} else {
this.hasEmptyValue = false
}
}

componentDidLoad() {
raf(() => this.checkSlotContent())
}

/**
* @internal
* Focuses the native <button> element
Expand All @@ -117,6 +117,14 @@ export class SegmentItem implements ComponentInterface {
}
}

/**
* @internal
*/
@Method()
async setAriaForm(ariaForm: BalAriaForm): Promise<void> {
this.ariaForm = { ...ariaForm }
}

private updateVertical = (ev: BalEvents.BalSegmentVertical) => {
this.isVertical = ev.detail
}
Expand All @@ -126,7 +134,7 @@ export class SegmentItem implements ComponentInterface {

if (segmentEl) {
if (segmentEl.value === '' || segmentEl.value === undefined || segmentEl.value === null) {
const items = this.items
const items = this.allAvailableOptions
if (items.length > 0) {
const first = items[0]
this.isFocusable = first === this.el
Expand All @@ -139,53 +147,26 @@ export class SegmentItem implements ComponentInterface {
if (segmentEl.disabled) {
this.disabled = true
}

this.isLast = segmentEl.lastElementChild === this.el
this.isFirst = segmentEl.firstElementChild === this.el

this.calculateEmptyValue()
}
}

private get items() {
return this.allItems.filter(item => !item.disabled)
private get allAvailableOptions() {
return this.allOptions.filter(item => !item.disabled)
}

private get allItems() {
private get allOptions() {
const { segmentEl } = this
if (segmentEl) {
return Array.from(segmentEl.querySelectorAll('bal-segment-item'))
}
return []
}

private isFirst() {
const { segmentEl } = this
let items = this.items

if (segmentEl && segmentEl.disabled) {
items = this.allItems
}

if (items.length > 0) {
const first = items[0]
return first === this.el
}

return false
}

private isLast() {
const { segmentEl } = this
let items = this.items

if (segmentEl && segmentEl.disabled) {
items = this.allItems
}

if (items.length > 0) {
const last = items[items.length - 1]
return last === this.el
}

return false
}

private checkSlotContent() {
const slot = this.el.querySelector('[part="slot"]') as HTMLSpanElement
const children = slot ? slot.innerHTML.trim() : ''
Expand All @@ -202,7 +183,7 @@ export class SegmentItem implements ComponentInterface {
}

render() {
const { checked, focused, segmentEl, label, isFocusable } = this
const { checked, focused, segmentEl, label, isFocusable, isFirst, hasEmptyValue } = this
const block = BEM.block('segment-item')
const buttonBem = block.element('button')
const indicatorBem = block.element('indicator')
Expand All @@ -211,6 +192,21 @@ export class SegmentItem implements ComponentInterface {
const disabled = this.disabled || (segmentEl && segmentEl.disabled)
const vertical = this.isVertical

const hasTabindex = (hasEmptyValue && isFirst) || (isFocusable && !disabled)

const id = (hasTabindex && this.ariaForm.controlId) || this.inputId

let buttonAttributes: any = {}

if (hasTabindex) {
let labelId = this.ariaForm.labelId || null
labelId = `${labelId || ''} ${id}-lbl`.trim()
buttonAttributes = {
'aria-labelledby': labelId,
'aria-describedby': this.ariaForm.messageId,
}
}

return (
<Host
class={{
Expand All @@ -219,13 +215,14 @@ export class SegmentItem implements ComponentInterface {
...block.modifier('disabled').class(disabled),
...block.modifier('checked').class(checked),
...block.modifier('invalid').class(invalid),
...block.modifier('line').class(!this.isFirst() && !checked),
...block.modifier('last').class(this.isLast() && !checked),
...block.modifier('last').class(this.isLast && !checked),
}}
>
<button
id={id}
role="radio"
aria-checked={checked ? 'true' : 'false'}
{...buttonAttributes}
class={{
...buttonBem.class(),
...buttonBem.modifier('checked').class(checked),
Expand All @@ -234,11 +231,9 @@ export class SegmentItem implements ComponentInterface {
...buttonBem.modifier('focused').class(focused),
...buttonBem.modifier('vertical').class(vertical),
}}
aria-labelledby={`bal-si-${this.id}-label`}
type={'button'}
tabIndex={isFocusable ? 0 : -1}
tabIndex={hasTabindex ? 0 : -1}
part="native"
onBlur={ev => this.balBlur.emit(ev)}
disabled={disabled}
ref={el => (this.nativeEl = el)}
{...this.inheritedAttributes}
Expand All @@ -255,7 +250,7 @@ export class SegmentItem implements ComponentInterface {
></bal-icon>
<bal-stack space="x-small" layout={'horizontal'}>
<bal-content space="none">
<bal-label htmlId={`bal-si-${this.id}-label`}>{label}</bal-label>
<bal-label htmlId={`bal-si-${this.internalId}-label`}>{label}</bal-label>
<span part="slot" class={{ ...buttonBem.element('slot').modifier('hidden').class(!this.hasSlotContent) }}>
{' '}
<slot onSlotchange={this.onSlottedItemsChange}></slot>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ namespace BalEvents {

export type BalSegmentBlurDetail = FocusEvent
export type BalSegmentBlur = BalSegmentCustomEvent<BalSegmentBlurDetail>

export type BalSegmentFocusDetail = FocusEvent
export type BalSegmentFocus = BalSegmentCustomEvent<BalSegmentFocusDetail>
}
Loading